summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Shadura <andrewsh@debian.org>2017-09-22 17:42:02 +0200
committerAndrew Shadura <andrewsh@debian.org>2017-09-22 17:42:02 +0200
commitfb2783bc10e51d2bcd5b03f4f4262ee441a905c4 (patch)
tree9c9e52ee7f0a0d299c8290ca05f0d3341d2fa017
New upstream version 1.0.1
-rw-r--r--.travis.yml4
-rw-r--r--CHANGES.md46
-rw-r--r--LICENSE22
-rw-r--r--README.md83
-rw-r--r--api_declaration_list.go64
-rw-r--r--config.go46
-rw-r--r--model_builder.go467
-rw-r--r--model_builder_test.go1283
-rw-r--r--model_list.go86
-rw-r--r--model_list_test.go48
-rw-r--r--model_property_ext.go81
-rw-r--r--model_property_ext_test.go70
-rw-r--r--model_property_list.go87
-rw-r--r--model_property_list_test.go47
-rw-r--r--ordered_route_map.go36
-rw-r--r--ordered_route_map_test.go29
-rw-r--r--postbuild_model_test.go42
-rw-r--r--swagger.go185
-rw-r--r--swagger_builder.go21
-rw-r--r--swagger_test.go318
-rw-r--r--swagger_webservice.go443
-rw-r--r--test_package/struct.go5
-rw-r--r--utils_test.go86
23 files changed, 3599 insertions, 0 deletions
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..c74e4fa
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,4 @@
+language: go
+
+go:
+ - 1.x \ No newline at end of file
diff --git a/CHANGES.md b/CHANGES.md
new file mode 100644
index 0000000..213b8e7
--- /dev/null
+++ b/CHANGES.md
@@ -0,0 +1,46 @@
+Change history of swagger
+=
+2017-01-30
+- moved from go-restful/swagger to go-restful-swagger12
+
+2015-10-16
+- add type override mechanism for swagger models (MR 254, nathanejohnson)
+- replace uses of wildcard in generated apidocs (issue 251)
+
+2015-05-25
+- (api break) changed the type of Properties in Model
+- (api break) changed the type of Models in ApiDeclaration
+- (api break) changed the parameter type of PostBuildDeclarationMapFunc
+
+2015-04-09
+- add ModelBuildable interface for customization of Model
+
+2015-03-17
+- preserve order of Routes per WebService in Swagger listing
+- fix use of $ref and type in Swagger models
+- add api version to listing
+
+2014-11-14
+- operation parameters are now sorted using ordering path,query,form,header,body
+
+2014-11-12
+- respect omitempty tag value for embedded structs
+- expose ApiVersion of WebService to Swagger ApiDeclaration
+
+2014-05-29
+- (api add) Ability to define custom http.Handler to serve swagger-ui static files
+
+2014-05-04
+- (fix) include model for array element type of response
+
+2014-01-03
+- (fix) do not add primitive type to the Api models
+
+2013-11-27
+- (fix) make Swagger work for WebServices with root ("/" or "") paths
+
+2013-10-29
+- (api add) package variable LogInfo to customize logging function
+
+2013-10-15
+- upgraded to spec version 1.2 (https://github.com/wordnik/swagger-core/wiki/1.2-transition) \ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..aeab5b4
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2017 Ernest Micklei
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..cad2896
--- /dev/null
+++ b/README.md
@@ -0,0 +1,83 @@
+# go-restful-swagger12
+
+[![Build Status](https://travis-ci.org/emicklei/go-restful-swagger12.png)](https://travis-ci.org/emicklei/go-restful-swagger12)
+[![GoDoc](https://godoc.org/github.com/emicklei/go-restful-swagger12?status.svg)](https://godoc.org/github.com/emicklei/go-restful-swagger12)
+
+How to use Swagger UI with go-restful
+=
+
+Get the Swagger UI sources (version 1.2 only)
+
+ git clone https://github.com/wordnik/swagger-ui.git
+
+The project contains a "dist" folder.
+Its contents has all the Swagger UI files you need.
+
+The `index.html` has an `url` set to `http://petstore.swagger.wordnik.com/api/api-docs`.
+You need to change that to match your WebService JSON endpoint e.g. `http://localhost:8080/apidocs.json`
+
+Now, you can install the Swagger WebService for serving the Swagger specification in JSON.
+
+ config := swagger.Config{
+ WebServices: restful.RegisteredWebServices(),
+ ApiPath: "/apidocs.json",
+ SwaggerPath: "/apidocs/",
+ SwaggerFilePath: "/Users/emicklei/Projects/swagger-ui/dist"}
+ swagger.InstallSwaggerService(config)
+
+
+Documenting Structs
+--
+
+Currently there are 2 ways to document your structs in the go-restful Swagger.
+
+###### By using struct tags
+- Use tag "description" to annotate a struct field with a description to show in the UI
+- Use tag "modelDescription" to annotate the struct itself with a description to show in the UI. The tag can be added in an field of the struct and in case that there are multiple definition, they will be appended with an empty line.
+
+###### By using the SwaggerDoc method
+Here is an example with an `Address` struct and the documentation for each of the fields. The `""` is a special entry for **documenting the struct itself**.
+
+ type Address struct {
+ Country string `json:"country,omitempty"`
+ PostCode int `json:"postcode,omitempty"`
+ }
+
+ func (Address) SwaggerDoc() map[string]string {
+ return map[string]string{
+ "": "Address doc",
+ "country": "Country doc",
+ "postcode": "PostCode doc",
+ }
+ }
+
+This example will generate a JSON like this
+
+ {
+ "Address": {
+ "id": "Address",
+ "description": "Address doc",
+ "properties": {
+ "country": {
+ "type": "string",
+ "description": "Country doc"
+ },
+ "postcode": {
+ "type": "integer",
+ "format": "int32",
+ "description": "PostCode doc"
+ }
+ }
+ }
+ }
+
+**Very Important Notes:**
+- `SwaggerDoc()` is using a **NON-Pointer** receiver (e.g. func (Address) and not func (*Address))
+- The returned map should use as key the name of the field as defined in the JSON parameter (e.g. `"postcode"` and not `"PostCode"`)
+
+Notes
+--
+- The Nickname of an Operation is automatically set by finding the name of the function. You can override it using RouteBuilder.Operation(..)
+- The WebServices field of swagger.Config can be used to control which service you want to expose and document ; you can have multiple configs and therefore multiple endpoints.
+
+© 2017, ernestmicklei.com. MIT License. Contributions welcome. \ No newline at end of file
diff --git a/api_declaration_list.go b/api_declaration_list.go
new file mode 100644
index 0000000..9f4c369
--- /dev/null
+++ b/api_declaration_list.go
@@ -0,0 +1,64 @@
+package swagger
+
+// Copyright 2015 Ernest Micklei. All rights reserved.
+// Use of this source code is governed by a license
+// that can be found in the LICENSE file.
+
+import (
+ "bytes"
+ "encoding/json"
+)
+
+// ApiDeclarationList maintains an ordered list of ApiDeclaration.
+type ApiDeclarationList struct {
+ List []ApiDeclaration
+}
+
+// At returns the ApiDeclaration by its path unless absent, then ok is false
+func (l *ApiDeclarationList) At(path string) (a ApiDeclaration, ok bool) {
+ for _, each := range l.List {
+ if each.ResourcePath == path {
+ return each, true
+ }
+ }
+ return a, false
+}
+
+// Put adds or replaces a ApiDeclaration with this name
+func (l *ApiDeclarationList) Put(path string, a ApiDeclaration) {
+ // maybe replace existing
+ for i, each := range l.List {
+ if each.ResourcePath == path {
+ // replace
+ l.List[i] = a
+ return
+ }
+ }
+ // add
+ l.List = append(l.List, a)
+}
+
+// Do enumerates all the properties, each with its assigned name
+func (l *ApiDeclarationList) Do(block func(path string, decl ApiDeclaration)) {
+ for _, each := range l.List {
+ block(each.ResourcePath, each)
+ }
+}
+
+// MarshalJSON writes the ModelPropertyList as if it was a map[string]ModelProperty
+func (l ApiDeclarationList) MarshalJSON() ([]byte, error) {
+ var buf bytes.Buffer
+ encoder := json.NewEncoder(&buf)
+ buf.WriteString("{\n")
+ for i, each := range l.List {
+ buf.WriteString("\"")
+ buf.WriteString(each.ResourcePath)
+ buf.WriteString("\": ")
+ encoder.Encode(each)
+ if i < len(l.List)-1 {
+ buf.WriteString(",\n")
+ }
+ }
+ buf.WriteString("}")
+ return buf.Bytes(), nil
+}
diff --git a/config.go b/config.go
new file mode 100644
index 0000000..18f8e57
--- /dev/null
+++ b/config.go
@@ -0,0 +1,46 @@
+package swagger
+
+import (
+ "net/http"
+ "reflect"
+
+ "github.com/emicklei/go-restful"
+)
+
+// PostBuildDeclarationMapFunc can be used to modify the api declaration map.
+type PostBuildDeclarationMapFunc func(apiDeclarationMap *ApiDeclarationList)
+
+// MapSchemaFormatFunc can be used to modify typeName at definition time.
+type MapSchemaFormatFunc func(typeName string) string
+
+// MapModelTypeNameFunc can be used to return the desired typeName for a given
+// type. It will return false if the default name should be used.
+type MapModelTypeNameFunc func(t reflect.Type) (string, bool)
+
+type Config struct {
+ // url where the services are available, e.g. http://localhost:8080
+ // if left empty then the basePath of Swagger is taken from the actual request
+ WebServicesUrl string
+ // path where the JSON api is avaiable , e.g. /apidocs
+ ApiPath string
+ // [optional] path where the swagger UI will be served, e.g. /swagger
+ SwaggerPath string
+ // [optional] location of folder containing Swagger HTML5 application index.html
+ SwaggerFilePath string
+ // api listing is constructed from this list of restful WebServices.
+ WebServices []*restful.WebService
+ // will serve all static content (scripts,pages,images)
+ StaticHandler http.Handler
+ // [optional] on default CORS (Cross-Origin-Resource-Sharing) is enabled.
+ DisableCORS bool
+ // Top-level API version. Is reflected in the resource listing.
+ ApiVersion string
+ // If set then call this handler after building the complete ApiDeclaration Map
+ PostBuildHandler PostBuildDeclarationMapFunc
+ // Swagger global info struct
+ Info Info
+ // [optional] If set, model builder should call this handler to get addition typename-to-swagger-format-field conversion.
+ SchemaFormatHandler MapSchemaFormatFunc
+ // [optional] If set, model builder should call this handler to retrieve the name for a given type.
+ ModelTypeNameHandler MapModelTypeNameFunc
+}
diff --git a/model_builder.go b/model_builder.go
new file mode 100644
index 0000000..d40786f
--- /dev/null
+++ b/model_builder.go
@@ -0,0 +1,467 @@
+package swagger
+
+import (
+ "encoding/json"
+ "reflect"
+ "strings"
+)
+
+// ModelBuildable is used for extending Structs that need more control over
+// how the Model appears in the Swagger api declaration.
+type ModelBuildable interface {
+ PostBuildModel(m *Model) *Model
+}
+
+type modelBuilder struct {
+ Models *ModelList
+ Config *Config
+}
+
+type documentable interface {
+ SwaggerDoc() map[string]string
+}
+
+// Check if this structure has a method with signature func (<theModel>) SwaggerDoc() map[string]string
+// If it exists, retrive the documentation and overwrite all struct tag descriptions
+func getDocFromMethodSwaggerDoc2(model reflect.Type) map[string]string {
+ if docable, ok := reflect.New(model).Elem().Interface().(documentable); ok {
+ return docable.SwaggerDoc()
+ }
+ return make(map[string]string)
+}
+
+// addModelFrom creates and adds a Model to the builder and detects and calls
+// the post build hook for customizations
+func (b modelBuilder) addModelFrom(sample interface{}) {
+ if modelOrNil := b.addModel(reflect.TypeOf(sample), ""); modelOrNil != nil {
+ // allow customizations
+ if buildable, ok := sample.(ModelBuildable); ok {
+ modelOrNil = buildable.PostBuildModel(modelOrNil)
+ b.Models.Put(modelOrNil.Id, *modelOrNil)
+ }
+ }
+}
+
+func (b modelBuilder) addModel(st reflect.Type, nameOverride string) *Model {
+ // Turn pointers into simpler types so further checks are
+ // correct.
+ if st.Kind() == reflect.Ptr {
+ st = st.Elem()
+ }
+
+ modelName := b.keyFrom(st)
+ if nameOverride != "" {
+ modelName = nameOverride
+ }
+ // no models needed for primitive types
+ if b.isPrimitiveType(modelName) {
+ return nil
+ }
+ // golang encoding/json packages says array and slice values encode as
+ // JSON arrays, except that []byte encodes as a base64-encoded string.
+ // If we see a []byte here, treat it at as a primitive type (string)
+ // and deal with it in buildArrayTypeProperty.
+ if (st.Kind() == reflect.Slice || st.Kind() == reflect.Array) &&
+ st.Elem().Kind() == reflect.Uint8 {
+ return nil
+ }
+ // see if we already have visited this model
+ if _, ok := b.Models.At(modelName); ok {
+ return nil
+ }
+ sm := Model{
+ Id: modelName,
+ Required: []string{},
+ Properties: ModelPropertyList{}}
+
+ // reference the model before further initializing (enables recursive structs)
+ b.Models.Put(modelName, sm)
+
+ // check for slice or array
+ if st.Kind() == reflect.Slice || st.Kind() == reflect.Array {
+ b.addModel(st.Elem(), "")
+ return &sm
+ }
+ // check for structure or primitive type
+ if st.Kind() != reflect.Struct {
+ return &sm
+ }
+
+ fullDoc := getDocFromMethodSwaggerDoc2(st)
+ modelDescriptions := []string{}
+
+ for i := 0; i < st.NumField(); i++ {
+ field := st.Field(i)
+ jsonName, modelDescription, prop := b.buildProperty(field, &sm, modelName)
+ if len(modelDescription) > 0 {
+ modelDescriptions = append(modelDescriptions, modelDescription)
+ }
+
+ // add if not omitted
+ if len(jsonName) != 0 {
+ // update description
+ if fieldDoc, ok := fullDoc[jsonName]; ok {
+ prop.Description = fieldDoc
+ }
+ // update Required
+ if b.isPropertyRequired(field) {
+ sm.Required = append(sm.Required, jsonName)
+ }
+ sm.Properties.Put(jsonName, prop)
+ }
+ }
+
+ // We always overwrite documentation if SwaggerDoc method exists
+ // "" is special for documenting the struct itself
+ if modelDoc, ok := fullDoc[""]; ok {
+ sm.Description = modelDoc
+ } else if len(modelDescriptions) != 0 {
+ sm.Description = strings.Join(modelDescriptions, "\n")
+ }
+
+ // update model builder with completed model
+ b.Models.Put(modelName, sm)
+
+ return &sm
+}
+
+func (b modelBuilder) isPropertyRequired(field reflect.StructField) bool {
+ required := true
+ if jsonTag := field.Tag.Get("json"); jsonTag != "" {
+ s := strings.Split(jsonTag, ",")
+ if len(s) > 1 && s[1] == "omitempty" {
+ return false
+ }
+ }
+ return required
+}
+
+func (b modelBuilder) buildProperty(field reflect.StructField, model *Model, modelName string) (jsonName, modelDescription string, prop ModelProperty) {
+ jsonName = b.jsonNameOfField(field)
+ if len(jsonName) == 0 {
+ // empty name signals skip property
+ return "", "", prop
+ }
+
+ if field.Name == "XMLName" && field.Type.String() == "xml.Name" {
+ // property is metadata for the xml.Name attribute, can be skipped
+ return "", "", prop
+ }
+
+ if tag := field.Tag.Get("modelDescription"); tag != "" {
+ modelDescription = tag
+ }
+
+ prop.setPropertyMetadata(field)
+ if prop.Type != nil {
+ return jsonName, modelDescription, prop
+ }
+ fieldType := field.Type
+
+ // check if type is doing its own marshalling
+ marshalerType := reflect.TypeOf((*json.Marshaler)(nil)).Elem()
+ if fieldType.Implements(marshalerType) {
+ var pType = "string"
+ if prop.Type == nil {
+ prop.Type = &pType
+ }
+ if prop.Format == "" {
+ prop.Format = b.jsonSchemaFormat(b.keyFrom(fieldType))
+ }
+ return jsonName, modelDescription, prop
+ }
+
+ // check if annotation says it is a string
+ if jsonTag := field.Tag.Get("json"); jsonTag != "" {
+ s := strings.Split(jsonTag, ",")
+ if len(s) > 1 && s[1] == "string" {
+ stringt := "string"
+ prop.Type = &stringt
+ return jsonName, modelDescription, prop
+ }
+ }
+
+ fieldKind := fieldType.Kind()
+ switch {
+ case fieldKind == reflect.Struct:
+ jsonName, prop := b.buildStructTypeProperty(field, jsonName, model)
+ return jsonName, modelDescription, prop
+ case fieldKind == reflect.Slice || fieldKind == reflect.Array:
+ jsonName, prop := b.buildArrayTypeProperty(field, jsonName, modelName)
+ return jsonName, modelDescription, prop
+ case fieldKind == reflect.Ptr:
+ jsonName, prop := b.buildPointerTypeProperty(field, jsonName, modelName)
+ return jsonName, modelDescription, prop
+ case fieldKind == reflect.String:
+ stringt := "string"
+ prop.Type = &stringt
+ return jsonName, modelDescription, prop
+ case fieldKind == reflect.Map:
+ // if it's a map, it's unstructured, and swagger 1.2 can't handle it
+ objectType := "object"
+ prop.Type = &objectType
+ return jsonName, modelDescription, prop
+ }
+
+ fieldTypeName := b.keyFrom(fieldType)
+ if b.isPrimitiveType(fieldTypeName) {
+ mapped := b.jsonSchemaType(fieldTypeName)
+ prop.Type = &mapped
+ prop.Format = b.jsonSchemaFormat(fieldTypeName)
+ return jsonName, modelDescription, prop
+ }
+ modelType := b.keyFrom(fieldType)
+ prop.Ref = &modelType
+
+ if fieldType.Name() == "" { // override type of anonymous structs
+ nestedTypeName := modelName + "." + jsonName
+ prop.Ref = &nestedTypeName
+ b.addModel(fieldType, nestedTypeName)
+ }
+ return jsonName, modelDescription, prop
+}
+
+func hasNamedJSONTag(field reflect.StructField) bool {
+ parts := strings.Split(field.Tag.Get("json"), ",")
+ if len(parts) == 0 {
+ return false
+ }
+ for _, s := range parts[1:] {
+ if s == "inline" {
+ return false
+ }
+ }
+ return len(parts[0]) > 0
+}
+
+func (b modelBuilder) buildStructTypeProperty(field reflect.StructField, jsonName string, model *Model) (nameJson string, prop ModelProperty) {
+ prop.setPropertyMetadata(field)
+ // Check for type override in tag
+ if prop.Type != nil {
+ return jsonName, prop
+ }
+ fieldType := field.Type
+ // check for anonymous
+ if len(fieldType.Name()) == 0 {
+ // anonymous
+ anonType := model.Id + "." + jsonName
+ b.addModel(fieldType, anonType)
+ prop.Ref = &anonType
+ return jsonName, prop
+ }
+
+ if field.Name == fieldType.Name() && field.Anonymous && !hasNamedJSONTag(field) {
+ // embedded struct
+ sub := modelBuilder{new(ModelList), b.Config}
+ sub.addModel(fieldType, "")
+ subKey := sub.keyFrom(fieldType)
+ // merge properties from sub
+ subModel, _ := sub.Models.At(subKey)
+ subModel.Properties.Do(func(k string, v ModelProperty) {
+ model.Properties.Put(k, v)
+ // if subModel says this property is required then include it
+ required := false
+ for _, each := range subModel.Required {
+ if k == each {
+ required = true
+ break
+ }
+ }
+ if required {
+ model.Required = append(model.Required, k)
+ }
+ })
+ // add all new referenced models
+ sub.Models.Do(func(key string, sub Model) {
+ if key != subKey {
+ if _, ok := b.Models.At(key); !ok {
+ b.Models.Put(key, sub)
+ }
+ }
+ })
+ // empty name signals skip property
+ return "", prop
+ }
+ // simple struct
+ b.addModel(fieldType, "")
+ var pType = b.keyFrom(fieldType)
+ prop.Ref = &pType
+ return jsonName, prop
+}
+
+func (b modelBuilder) buildArrayTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop ModelProperty) {
+ // check for type override in tags
+ prop.setPropertyMetadata(field)
+ if prop.Type != nil {
+ return jsonName, prop
+ }
+ fieldType := field.Type
+ if fieldType.Elem().Kind() == reflect.Uint8 {
+ stringt := "string"
+ prop.Type = &stringt
+ return jsonName, prop
+ }
+ var pType = "array"
+ prop.Type = &pType
+ isPrimitive := b.isPrimitiveType(fieldType.Elem().Name())
+ elemTypeName := b.getElementTypeName(modelName, jsonName, fieldType.Elem())
+ prop.Items = new(Item)
+ if isPrimitive {
+ mapped := b.jsonSchemaType(elemTypeName)
+ prop.Items.Type = &mapped
+ } else {
+ prop.Items.Ref = &elemTypeName
+ }
+ // add|overwrite model for element type
+ if fieldType.Elem().Kind() == reflect.Ptr {
+ fieldType = fieldType.Elem()
+ }
+ if !isPrimitive {
+ b.addModel(fieldType.Elem(), elemTypeName)
+ }
+ return jsonName, prop
+}
+
+func (b modelBuilder) buildPointerTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop ModelProperty) {
+ prop.setPropertyMetadata(field)
+ // Check for type override in tags
+ if prop.Type != nil {
+ return jsonName, prop
+ }
+ fieldType := field.Type
+
+ // override type of pointer to list-likes
+ if fieldType.Elem().Kind() == reflect.Slice || fieldType.Elem().Kind() == reflect.Array {
+ var pType = "array"
+ prop.Type = &pType
+ isPrimitive := b.isPrimitiveType(fieldType.Elem().Elem().Name())
+ elemName := b.getElementTypeName(modelName, jsonName, fieldType.Elem().Elem())
+ if isPrimitive {
+ primName := b.jsonSchemaType(elemName)
+ prop.Items = &Item{Ref: &primName}
+ } else {
+ prop.Items = &Item{Ref: &elemName}
+ }
+ if !isPrimitive {
+ // add|overwrite model for element type
+ b.addModel(fieldType.Elem().Elem(), elemName)
+ }
+ } else {
+ // non-array, pointer type
+ fieldTypeName := b.keyFrom(fieldType.Elem())
+ var pType = b.jsonSchemaType(fieldTypeName) // no star, include pkg path
+ if b.isPrimitiveType(fieldTypeName) {
+ prop.Type = &pType
+ prop.Format = b.jsonSchemaFormat(fieldTypeName)
+ return jsonName, prop
+ }
+ prop.Ref = &pType
+ elemName := ""
+ if fieldType.Elem().Name() == "" {
+ elemName = modelName + "." + jsonName
+ prop.Ref = &elemName
+ }
+ b.addModel(fieldType.Elem(), elemName)
+ }
+ return jsonName, prop
+}
+
+func (b modelBuilder) getElementTypeName(modelName, jsonName string, t reflect.Type) string {
+ if t.Kind() == reflect.Ptr {
+ t = t.Elem()
+ }
+ if t.Name() == "" {
+ return modelName + "." + jsonName
+ }
+ return b.keyFrom(t)
+}
+
+func (b modelBuilder) keyFrom(st reflect.Type) string {
+ key := st.String()
+ if b.Config != nil && b.Config.ModelTypeNameHandler != nil {
+ if name, ok := b.Config.ModelTypeNameHandler(st); ok {
+ key = name
+ }
+ }
+ if len(st.Name()) == 0 { // unnamed type
+ // Swagger UI has special meaning for [
+ key = strings.Replace(key, "[]", "||", -1)
+ }
+ return key
+}
+
+// see also https://golang.org/ref/spec#Numeric_types
+func (b modelBuilder) isPrimitiveType(modelName string) bool {
+ if len(modelName) == 0 {
+ return false
+ }
+ return strings.Contains("uint uint8 uint16 uint32 uint64 int int8 int16 int32 int64 float32 float64 bool string byte rune time.Time", modelName)
+}
+
+// jsonNameOfField returns the name of the field as it should appear in JSON format
+// An empty string indicates that this field is not part of the JSON representation
+func (b modelBuilder) jsonNameOfField(field reflect.StructField) string {
+ if jsonTag := field.Tag.Get("json"); jsonTag != "" {
+ s := strings.Split(jsonTag, ",")
+ if s[0] == "-" {
+ // empty name signals skip property
+ return ""
+ } else if s[0] != "" {
+ return s[0]
+ }
+ }
+ return field.Name
+}
+
+// see also http://json-schema.org/latest/json-schema-core.html#anchor8
+func (b modelBuilder) jsonSchemaType(modelName string) string {
+ schemaMap := map[string]string{
+ "uint": "integer",
+ "uint8": "integer",
+ "uint16": "integer",
+ "uint32": "integer",
+ "uint64": "integer",
+
+ "int": "integer",
+ "int8": "integer",
+ "int16": "integer",
+ "int32": "integer",
+ "int64": "integer",
+
+ "byte": "integer",
+ "float64": "number",
+ "float32": "number",
+ "bool": "boolean",
+ "time.Time": "string",
+ }
+ mapped, ok := schemaMap[modelName]
+ if !ok {
+ return modelName // use as is (custom or struct)
+ }
+ return mapped
+}
+
+func (b modelBuilder) jsonSchemaFormat(modelName string) string {
+ if b.Config != nil && b.Config.SchemaFormatHandler != nil {
+ if mapped := b.Config.SchemaFormatHandler(modelName); mapped != "" {
+ return mapped
+ }
+ }
+ schemaMap := map[string]string{
+ "int": "int32",
+ "int32": "int32",
+ "int64": "int64",
+ "byte": "byte",
+ "uint": "integer",
+ "uint8": "byte",
+ "float64": "double",
+ "float32": "float",
+ "time.Time": "date-time",
+ "*time.Time": "date-time",
+ }
+ mapped, ok := schemaMap[modelName]
+ if !ok {
+ return "" // no format
+ }
+ return mapped
+}
diff --git a/model_builder_test.go b/model_builder_test.go
new file mode 100644
index 0000000..a2d2f12
--- /dev/null
+++ b/model_builder_test.go
@@ -0,0 +1,1283 @@
+package swagger
+
+import (
+ "encoding/xml"
+ "net"
+ "reflect"
+ "testing"
+ "time"
+)
+
+type YesNo bool
+
+func (y YesNo) MarshalJSON() ([]byte, error) {
+ if y {
+ return []byte("yes"), nil
+ }
+ return []byte("no"), nil
+}
+
+// clear && go test -v -test.run TestRef_Issue190 ...swagger
+func TestRef_Issue190(t *testing.T) {
+ type User struct {
+ items []string
+ }
+ testJsonFromStruct(t, User{}, `{
+ "swagger.User": {
+ "id": "swagger.User",
+ "required": [
+ "items"
+ ],
+ "properties": {
+ "items": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }`)
+}
+
+func TestWithoutAdditionalFormat(t *testing.T) {
+ type mytime struct {
+ time.Time
+ }
+ type usemytime struct {
+ t mytime
+ }
+ testJsonFromStruct(t, usemytime{}, `{
+ "swagger.usemytime": {
+ "id": "swagger.usemytime",
+ "required": [
+ "t"
+ ],
+ "properties": {
+ "t": {
+ "type": "string"
+ }
+ }
+ }
+ }`)
+}
+
+func TestWithAdditionalFormat(t *testing.T) {
+ type mytime struct {
+ time.Time
+ }
+ type usemytime struct {
+ t mytime
+ }
+ testJsonFromStructWithConfig(t, usemytime{}, `{
+ "swagger.usemytime": {
+ "id": "swagger.usemytime",
+ "required": [
+ "t"
+ ],
+ "properties": {
+ "t": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ }
+ }`, &Config{
+ SchemaFormatHandler: func(typeName string) string {
+ switch typeName {
+ case "swagger.mytime":
+ return "date-time"
+ }
+ return ""
+ },
+ })
+}
+
+// clear && go test -v -test.run TestCustomMarshaller_Issue96 ...swagger
+func TestCustomMarshaller_Issue96(t *testing.T) {
+ type Vote struct {
+ What YesNo
+ }
+ testJsonFromStruct(t, Vote{}, `{
+ "swagger.Vote": {
+ "id": "swagger.Vote",
+ "required": [
+ "What"
+ ],
+ "properties": {
+ "What": {
+ "type": "string"
+ }
+ }
+ }
+ }`)
+}
+
+// clear && go test -v -test.run TestPrimitiveTypes ...swagger
+func TestPrimitiveTypes(t *testing.T) {
+ type Prims struct {
+ f float64
+ t time.Time
+ }
+ testJsonFromStruct(t, Prims{}, `{
+ "swagger.Prims": {
+ "id": "swagger.Prims",
+ "required": [
+ "f",
+ "t"
+ ],
+ "properties": {
+ "f": {
+ "type": "number",
+ "format": "double"
+ },
+ "t": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ }
+ }`)
+}
+
+// clear && go test -v -test.run TestPrimitivePtrTypes ...swagger
+func TestPrimitivePtrTypes(t *testing.T) {
+ type Prims struct {
+ f *float64
+ t *time.Time
+ b *bool
+ s *string
+ i *int
+ }
+ testJsonFromStruct(t, Prims{}, `{
+ "swagger.Prims": {
+ "id": "swagger.Prims",
+ "required": [
+ "f",
+ "t",
+ "b",
+ "s",
+ "i"
+ ],
+ "properties": {
+ "b": {
+ "type": "boolean"
+ },
+ "f": {
+ "type": "number",
+ "format": "double"
+ },
+ "i": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "s": {
+ "type": "string"
+ },
+ "t": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ }
+ }`)
+}
+
+// clear && go test -v -test.run TestS1 ...swagger
+func TestS1(t *testing.T) {
+ type S1 struct {
+ Id string
+ }
+ testJsonFromStruct(t, S1{}, `{
+ "swagger.S1": {
+ "id": "swagger.S1",
+ "required": [
+ "Id"
+ ],
+ "properties": {
+ "Id": {
+ "type": "string"
+ }
+ }
+ }
+ }`)
+}
+
+// clear && go test -v -test.run TestS2 ...swagger
+func TestS2(t *testing.T) {
+ type S2 struct {
+ Ids []string
+ }
+ testJsonFromStruct(t, S2{}, `{
+ "swagger.S2": {
+ "id": "swagger.S2",
+ "required": [
+ "Ids"
+ ],
+ "properties": {
+ "Ids": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }`)
+}
+
+// clear && go test -v -test.run TestS3 ...swagger
+func TestS3(t *testing.T) {
+ type NestedS3 struct {
+ Id string
+ }
+ type S3 struct {
+ Nested NestedS3
+ }
+ testJsonFromStruct(t, S3{}, `{
+ "swagger.NestedS3": {
+ "id": "swagger.NestedS3",
+ "required": [
+ "Id"
+ ],
+ "properties": {
+ "Id": {
+ "type": "string"
+ }
+ }
+ },
+ "swagger.S3": {
+ "id": "swagger.S3",
+ "required": [
+ "Nested"
+ ],
+ "properties": {
+ "Nested": {
+ "$ref": "swagger.NestedS3"
+ }
+ }
+ }
+ }`)
+}
+
+type sample struct {
+ id string `swagger:"required"` // TODO
+ items []item
+ rootItem item `json:"root" description:"root desc"`
+}
+
+type item struct {
+ itemName string `json:"name"`
+}
+
+// clear && go test -v -test.run TestSampleToModelAsJson ...swagger
+func TestSampleToModelAsJson(t *testing.T) {
+ testJsonFromStruct(t, sample{items: []item{}}, `{
+ "swagger.item": {
+ "id": "swagger.item",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ }
+ }
+ },
+ "swagger.sample": {
+ "id": "swagger.sample",
+ "required": [
+ "id",
+ "items",
+ "root"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "swagger.item"
+ }
+ },
+ "root": {
+ "$ref": "swagger.item",
+ "description": "root desc"
+ }
+ }
+ }
+ }`)
+}
+
+func TestJsonTags(t *testing.T) {
+ type X struct {
+ A string
+ B string `json:"-"`
+ C int `json:",string"`
+ D int `json:","`
+ }
+
+ expected := `{
+ "swagger.X": {
+ "id": "swagger.X",
+ "required": [
+ "A",
+ "C",
+ "D"
+ ],
+ "properties": {
+ "A": {
+ "type": "string"
+ },
+ "C": {
+ "type": "string"
+ },
+ "D": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ }
+ }`
+
+ testJsonFromStruct(t, X{}, expected)
+}
+
+func TestJsonTagOmitempty(t *testing.T) {
+ type X struct {
+ A int `json:",omitempty"`
+ B int `json:"C,omitempty"`
+ }
+
+ expected := `{
+ "swagger.X": {
+ "id": "swagger.X",
+ "properties": {
+ "A": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "C": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ }
+ }`
+
+ testJsonFromStruct(t, X{}, expected)
+}
+
+func TestJsonTagName(t *testing.T) {
+ type X struct {
+ A string `json:"B"`
+ }
+
+ expected := `{
+ "swagger.X": {
+ "id": "swagger.X",
+ "required": [
+ "B"
+ ],
+ "properties": {
+ "B": {
+ "type": "string"
+ }
+ }
+ }
+ }`
+
+ testJsonFromStruct(t, X{}, expected)
+}
+
+func TestAnonymousStruct(t *testing.T) {
+ type X struct {
+ A struct {
+ B int
+ }
+ }
+
+ expected := `{
+ "swagger.X": {
+ "id": "swagger.X",
+ "required": [
+ "A"
+ ],
+ "properties": {
+ "A": {
+ "$ref": "swagger.X.A"
+ }
+ }
+ },
+ "swagger.X.A": {
+ "id": "swagger.X.A",
+ "required": [
+ "B"
+ ],
+ "properties": {
+ "B": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ }
+ }`
+
+ testJsonFromStruct(t, X{}, expected)
+}
+
+func TestAnonymousPtrStruct(t *testing.T) {
+ type X struct {
+ A *struct {
+ B int
+ }
+ }
+
+ expected := `{
+ "swagger.X": {
+ "id": "swagger.X",
+ "required": [
+ "A"
+ ],
+ "properties": {
+ "A": {
+ "$ref": "swagger.X.A"
+ }
+ }
+ },
+ "swagger.X.A": {
+ "id": "swagger.X.A",
+ "required": [
+ "B"
+ ],
+ "properties": {
+ "B": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ }
+ }`
+
+ testJsonFromStruct(t, X{}, expected)
+}
+
+func TestAnonymousArrayStruct(t *testing.T) {
+ type X struct {
+ A []struct {
+ B int
+ }
+ }
+
+ expected := `{
+ "swagger.X": {
+ "id": "swagger.X",
+ "required": [
+ "A"
+ ],
+ "properties": {
+ "A": {
+ "type": "array",
+ "items": {
+ "$ref": "swagger.X.A"
+ }
+ }
+ }
+ },
+ "swagger.X.A": {
+ "id": "swagger.X.A",
+ "required": [
+ "B"
+ ],
+ "properties": {
+ "B": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ }
+ }`
+
+ testJsonFromStruct(t, X{}, expected)
+}
+
+func TestAnonymousPtrArrayStruct(t *testing.T) {
+ type X struct {
+ A *[]struct {
+ B int
+ }
+ }
+
+ expected := `{
+ "swagger.X": {
+ "id": "swagger.X",
+ "required": [
+ "A"
+ ],
+ "properties": {
+ "A": {
+ "type": "array",
+ "items": {
+ "$ref": "swagger.X.A"
+ }
+ }
+ }
+ },
+ "swagger.X.A": {
+ "id": "swagger.X.A",
+ "required": [
+ "B"
+ ],
+ "properties": {
+ "B": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ }
+ }`
+
+ testJsonFromStruct(t, X{}, expected)
+}
+
+// go test -v -test.run TestEmbeddedStruct_Issue98 ...swagger
+func TestEmbeddedStruct_Issue98(t *testing.T) {
+ type Y struct {
+ A int
+ }
+ type X struct {
+ Y
+ }
+ testJsonFromStruct(t, X{}, `{
+ "swagger.X": {
+ "id": "swagger.X",
+ "required": [
+ "A"
+ ],
+ "properties": {
+ "A": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ }
+ }`)
+}
+
+type Dataset struct {
+ Names []string
+}
+
+// clear && go test -v -test.run TestIssue85 ...swagger
+func TestIssue85(t *testing.T) {
+ anon := struct{ Datasets []Dataset }{}
+ testJsonFromStruct(t, anon, `{
+ "struct { Datasets ||swagger.Dataset }": {
+ "id": "struct { Datasets ||swagger.Dataset }",
+ "required": [
+ "Datasets"
+ ],
+ "properties": {
+ "Datasets": {
+ "type": "array",
+ "items": {
+ "$ref": "swagger.Dataset"
+ }
+ }
+ }
+ },
+ "swagger.Dataset": {
+ "id": "swagger.Dataset",
+ "required": [
+ "Names"
+ ],
+ "properties": {
+ "Names": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }`)
+}
+
+type File struct {
+ History []File
+ HistoryPtrs []*File
+}
+
+// go test -v -test.run TestRecursiveStructure ...swagger
+func TestRecursiveStructure(t *testing.T) {
+ testJsonFromStruct(t, File{}, `{
+ "swagger.File": {
+ "id": "swagger.File",
+ "required": [
+ "History",
+ "HistoryPtrs"
+ ],
+ "properties": {
+ "History": {
+ "type": "array",
+ "items": {
+ "$ref": "swagger.File"
+ }
+ },
+ "HistoryPtrs": {
+ "type": "array",
+ "items": {
+ "$ref": "swagger.File"
+ }
+ }
+ }
+ }
+ }`)
+}
+
+type A1 struct {
+ B struct {
+ Id int
+ Comment string `json:"comment,omitempty"`
+ }
+}
+
+// go test -v -test.run TestEmbeddedStructA1 ...swagger
+func TestEmbeddedStructA1(t *testing.T) {
+ testJsonFromStruct(t, A1{}, `{
+ "swagger.A1": {
+ "id": "swagger.A1",
+ "required": [
+ "B"
+ ],
+ "properties": {
+ "B": {
+ "$ref": "swagger.A1.B"
+ }
+ }
+ },
+ "swagger.A1.B": {
+ "id": "swagger.A1.B",
+ "required": [
+ "Id"
+ ],
+ "properties": {
+ "Id": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "comment": {
+ "type": "string"
+ }
+ }
+ }
+ }`)
+}
+
+type A2 struct {
+ C
+}
+type C struct {
+ Id int `json:"B"`
+ Comment string `json:"comment,omitempty"`
+ Secure bool `json:"secure"`
+}
+
+// go test -v -test.run TestEmbeddedStructA2 ...swagger
+func TestEmbeddedStructA2(t *testing.T) {
+ testJsonFromStruct(t, A2{}, `{
+ "swagger.A2": {
+ "id": "swagger.A2",
+ "required": [
+ "B",
+ "secure"
+ ],
+ "properties": {
+ "B": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "comment": {
+ "type": "string"
+ },
+ "secure": {
+ "type": "boolean"
+ }
+ }
+ }
+ }`)
+}
+
+type A3 struct {
+ B D
+}
+
+type D struct {
+ Id int
+}
+
+// clear && go test -v -test.run TestStructA3 ...swagger
+func TestStructA3(t *testing.T) {
+ testJsonFromStruct(t, A3{}, `{
+ "swagger.A3": {
+ "id": "swagger.A3",
+ "required": [
+ "B"
+ ],
+ "properties": {
+ "B": {
+ "$ref": "swagger.D"
+ }
+ }
+ },
+ "swagger.D": {
+ "id": "swagger.D",
+ "required": [
+ "Id"
+ ],
+ "properties": {
+ "Id": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ }
+ }`)
+}
+
+type A4 struct {
+ D "json:,inline"
+}
+
+// clear && go test -v -test.run TestStructA4 ...swagger
+func TestEmbeddedStructA4(t *testing.T) {
+ testJsonFromStruct(t, A4{}, `{
+ "swagger.A4": {
+ "id": "swagger.A4",
+ "required": [
+ "Id"
+ ],
+ "properties": {
+ "Id": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ }
+ }`)
+}
+
+type A5 struct {
+ D `json:"d"`
+}
+
+// clear && go test -v -test.run TestStructA5 ...swagger
+func TestEmbeddedStructA5(t *testing.T) {
+ testJsonFromStruct(t, A5{}, `{
+ "swagger.A5": {
+ "id": "swagger.A5",
+ "required": [
+ "d"
+ ],
+ "properties": {
+ "d": {
+ "$ref": "swagger.D"
+ }
+ }
+ },
+ "swagger.D": {
+ "id": "swagger.D",
+ "required": [
+ "Id"
+ ],
+ "properties": {
+ "Id": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ }
+ }`)
+}
+
+type D2 struct {
+ id int
+ D []D
+}
+
+type A6 struct {
+ D2 "json:,inline"
+}
+
+// clear && go test -v -test.run TestStructA4 ...swagger
+func TestEmbeddedStructA6(t *testing.T) {
+ testJsonFromStruct(t, A6{}, `{
+ "swagger.A6": {
+ "id": "swagger.A6",
+ "required": [
+ "id",
+ "D"
+ ],
+ "properties": {
+ "D": {
+ "type": "array",
+ "items": {
+ "$ref": "swagger.D"
+ }
+ },
+ "id": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ },
+ "swagger.D": {
+ "id": "swagger.D",
+ "required": [
+ "Id"
+ ],
+ "properties": {
+ "Id": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ }
+ }`)
+}
+
+type ObjectId []byte
+
+type Region struct {
+ Id ObjectId `bson:"_id" json:"id"`
+ Name string `bson:"name" json:"name"`
+ Type string `bson:"type" json:"type"`
+}
+
+// clear && go test -v -test.run TestRegion_Issue113 ...swagger
+func TestRegion_Issue113(t *testing.T) {
+ testJsonFromStruct(t, []Region{}, `{
+ "||swagger.Region": {
+ "id": "||swagger.Region",
+ "properties": {}
+ },
+ "swagger.Region": {
+ "id": "swagger.Region",
+ "required": [
+ "id",
+ "name",
+ "type"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ }
+ }
+ }
+ }`)
+}
+
+// clear && go test -v -test.run TestIssue158 ...swagger
+func TestIssue158(t *testing.T) {
+ type Address struct {
+ Country string `json:"country,omitempty"`
+ }
+
+ type Customer struct {
+ Name string `json:"name"`
+ Address Address `json:"address"`
+ }
+ expected := `{
+ "swagger.Address": {
+ "id": "swagger.Address",
+ "properties": {
+ "country": {
+ "type": "string"
+ }
+ }
+ },
+ "swagger.Customer": {
+ "id": "swagger.Customer",
+ "required": [
+ "name",
+ "address"
+ ],
+ "properties": {
+ "address": {
+ "$ref": "swagger.Address"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ }
+ }`
+ testJsonFromStruct(t, Customer{}, expected)
+}
+
+func TestPointers(t *testing.T) {
+ type Vote struct {
+ What YesNo
+ }
+ testJsonFromStruct(t, &Vote{}, `{
+ "swagger.Vote": {
+ "id": "swagger.Vote",
+ "required": [
+ "What"
+ ],
+ "properties": {
+ "What": {
+ "type": "string"
+ }
+ }
+ }
+ }`)
+}
+
+func TestSlices(t *testing.T) {
+ type Address struct {
+ Country string `json:"country,omitempty"`
+ }
+ expected := `{
+ "swagger.Address": {
+ "id": "swagger.Address",
+ "properties": {
+ "country": {
+ "type": "string"
+ }
+ }
+ },
+ "swagger.Customer": {
+ "id": "swagger.Customer",
+ "required": [
+ "name",
+ "addresses"
+ ],
+ "properties": {
+ "addresses": {
+ "type": "array",
+ "items": {
+ "$ref": "swagger.Address"
+ }
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ }
+ }`
+ // both slices (with pointer value and with type value) should have equal swagger representation
+ {
+ type Customer struct {
+ Name string `json:"name"`
+ Addresses []Address `json:"addresses"`
+ }
+ testJsonFromStruct(t, Customer{}, expected)
+ }
+ {
+ type Customer struct {
+ Name string `json:"name"`
+ Addresses []*Address `json:"addresses"`
+ }
+ testJsonFromStruct(t, Customer{}, expected)
+ }
+
+}
+
+type Name struct {
+ Value string
+}
+
+func (n Name) PostBuildModel(m *Model) *Model {
+ m.Description = "titles must be upcase"
+ return m
+}
+
+type TOC struct {
+ Titles []Name
+}
+
+type Discography struct {
+ Title Name
+ TOC
+}
+
+// clear && go test -v -test.run TestEmbeddedStructPull204 ...swagger
+func TestEmbeddedStructPull204(t *testing.T) {
+ b := Discography{}
+ testJsonFromStruct(t, b, `
+{
+ "swagger.Discography": {
+ "id": "swagger.Discography",
+ "required": [
+ "Title",
+ "Titles"
+ ],
+ "properties": {
+ "Title": {
+ "$ref": "swagger.Name"
+ },
+ "Titles": {
+ "type": "array",
+ "items": {
+ "$ref": "swagger.Name"
+ }
+ }
+ }
+ },
+ "swagger.Name": {
+ "id": "swagger.Name",
+ "required": [
+ "Value"
+ ],
+ "properties": {
+ "Value": {
+ "type": "string"
+ }
+ }
+ }
+ }
+`)
+}
+
+type AddressWithMethod struct {
+ Country string `json:"country,omitempty"`
+ PostCode int `json:"postcode,omitempty"`
+}
+
+func (AddressWithMethod) SwaggerDoc() map[string]string {
+ return map[string]string{
+ "": "Address doc",
+ "country": "Country doc",
+ "postcode": "PostCode doc",
+ }
+}
+
+func TestDocInMethodSwaggerDoc(t *testing.T) {
+ expected := `{
+ "swagger.AddressWithMethod": {
+ "id": "swagger.AddressWithMethod",
+ "description": "Address doc",
+ "properties": {
+ "country": {
+ "type": "string",
+ "description": "Country doc"
+ },
+ "postcode": {
+ "type": "integer",
+ "format": "int32",
+ "description": "PostCode doc"
+ }
+ }
+ }
+ }`
+ testJsonFromStruct(t, AddressWithMethod{}, expected)
+}
+
+type RefDesc struct {
+ f1 *int64 `description:"desc"`
+}
+
+func TestPtrDescription(t *testing.T) {
+ b := RefDesc{}
+ expected := `{
+ "swagger.RefDesc": {
+ "id": "swagger.RefDesc",
+ "required": [
+ "f1"
+ ],
+ "properties": {
+ "f1": {
+ "type": "integer",
+ "format": "int64",
+ "description": "desc"
+ }
+ }
+ }
+ }`
+ testJsonFromStruct(t, b, expected)
+}
+
+type A struct {
+ B `json:",inline"`
+ C1 `json:"metadata,omitempty"`
+}
+
+type B struct {
+ SB string
+}
+
+type C1 struct {
+ SC string
+}
+
+func (A) SwaggerDoc() map[string]string {
+ return map[string]string{
+ "": "A struct",
+ "B": "B field", // We should not get anything from this
+ "metadata": "C1 field",
+ }
+}
+
+func (B) SwaggerDoc() map[string]string {
+ return map[string]string{
+ "": "B struct",
+ "SB": "SB field",
+ }
+}
+
+func (C1) SwaggerDoc() map[string]string {
+ return map[string]string{
+ "": "C1 struct",
+ "SC": "SC field",
+ }
+}
+
+func TestNestedStructDescription(t *testing.T) {
+ expected := `
+{
+ "swagger.A": {
+ "id": "swagger.A",
+ "description": "A struct",
+ "required": [
+ "SB"
+ ],
+ "properties": {
+ "SB": {
+ "type": "string",
+ "description": "SB field"
+ },
+ "metadata": {
+ "$ref": "swagger.C1",
+ "description": "C1 field"
+ }
+ }
+ },
+ "swagger.C1": {
+ "id": "swagger.C1",
+ "description": "C1 struct",
+ "required": [
+ "SC"
+ ],
+ "properties": {
+ "SC": {
+ "type": "string",
+ "description": "SC field"
+ }
+ }
+ }
+ }
+`
+ testJsonFromStruct(t, A{}, expected)
+}
+
+// This tests a primitive with type overrides in the struct tags
+type FakeInt int
+type E struct {
+ Id FakeInt `type:"integer"`
+ IP net.IP `type:"string"`
+}
+
+func TestOverridenTypeTagE1(t *testing.T) {
+ expected := `
+{
+ "swagger.E": {
+ "id": "swagger.E",
+ "required": [
+ "Id",
+ "IP"
+ ],
+ "properties": {
+ "Id": {
+ "type": "integer"
+ },
+ "IP": {
+ "type": "string"
+ }
+ }
+ }
+ }
+`
+ testJsonFromStruct(t, E{}, expected)
+}
+
+type XmlNamed struct {
+ XMLName xml.Name `xml:"user"`
+ Id string `json:"id" xml:"id"`
+ Name string `json:"name" xml:"name"`
+}
+
+func TestXmlNameStructs(t *testing.T) {
+ expected := `
+{
+ "swagger.XmlNamed": {
+ "id": "swagger.XmlNamed",
+ "required": [
+ "id",
+ "name"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ }
+ }
+`
+ testJsonFromStruct(t, XmlNamed{}, expected)
+}
+
+func TestNameCustomization(t *testing.T) {
+ expected := `
+{
+ "swagger.A": {
+ "id": "swagger.A",
+ "description": "A struct",
+ "required": [
+ "SB"
+ ],
+ "properties": {
+ "SB": {
+ "type": "string",
+ "description": "SB field"
+ },
+ "metadata": {
+ "$ref": "new.swagger.SpecialC1",
+ "description": "C1 field"
+ }
+ }
+ },
+ "new.swagger.SpecialC1": {
+ "id": "new.swagger.SpecialC1",
+ "description": "C1 struct",
+ "required": [
+ "SC"
+ ],
+ "properties": {
+ "SC": {
+ "type": "string",
+ "description": "SC field"
+ }
+ }
+ }
+ }`
+
+ testJsonFromStructWithConfig(t, A{}, expected, &Config{
+ ModelTypeNameHandler: func(t reflect.Type) (string, bool) {
+ if t == reflect.TypeOf(C1{}) {
+ return "new.swagger.SpecialC1", true
+ }
+ return "", false
+ },
+ })
+}
diff --git a/model_list.go b/model_list.go
new file mode 100644
index 0000000..9bb6cb6
--- /dev/null
+++ b/model_list.go
@@ -0,0 +1,86 @@
+package swagger
+
+// Copyright 2015 Ernest Micklei. All rights reserved.
+// Use of this source code is governed by a license
+// that can be found in the LICENSE file.
+
+import (
+ "bytes"
+ "encoding/json"
+)
+
+// NamedModel associates a name with a Model (not using its Id)
+type NamedModel struct {
+ Name string
+ Model Model
+}
+
+// ModelList encapsulates a list of NamedModel (association)
+type ModelList struct {
+ List []NamedModel
+}
+
+// Put adds or replaces a Model by its name
+func (l *ModelList) Put(name string, model Model) {
+ for i, each := range l.List {
+ if each.Name == name {
+ // replace
+ l.List[i] = NamedModel{name, model}
+ return
+ }
+ }
+ // add
+ l.List = append(l.List, NamedModel{name, model})
+}
+
+// At returns a Model by its name, ok is false if absent
+func (l *ModelList) At(name string) (m Model, ok bool) {
+ for _, each := range l.List {
+ if each.Name == name {
+ return each.Model, true
+ }
+ }
+ return m, false
+}
+
+// Do enumerates all the models, each with its assigned name
+func (l *ModelList) Do(block func(name string, value Model)) {
+ for _, each := range l.List {
+ block(each.Name, each.Model)
+ }
+}
+
+// MarshalJSON writes the ModelList as if it was a map[string]Model
+func (l ModelList) MarshalJSON() ([]byte, error) {
+ var buf bytes.Buffer
+ encoder := json.NewEncoder(&buf)
+ buf.WriteString("{\n")
+ for i, each := range l.List {
+ buf.WriteString("\"")
+ buf.WriteString(each.Name)
+ buf.WriteString("\": ")
+ encoder.Encode(each.Model)
+ if i < len(l.List)-1 {
+ buf.WriteString(",\n")
+ }
+ }
+ buf.WriteString("}")
+ return buf.Bytes(), nil
+}
+
+// UnmarshalJSON reads back a ModelList. This is an expensive operation.
+func (l *ModelList) UnmarshalJSON(data []byte) error {
+ raw := map[string]interface{}{}
+ json.NewDecoder(bytes.NewReader(data)).Decode(&raw)
+ for k, v := range raw {
+ // produces JSON bytes for each value
+ data, err := json.Marshal(v)
+ if err != nil {
+ return err
+ }
+ var m Model
+ json.NewDecoder(bytes.NewReader(data)).Decode(&m)
+ l.Put(k, m)
+ }
+ return nil
+}
diff --git a/model_list_test.go b/model_list_test.go
new file mode 100644
index 0000000..9a9ab91
--- /dev/null
+++ b/model_list_test.go
@@ -0,0 +1,48 @@
+package swagger
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestModelList(t *testing.T) {
+ m := Model{}
+ m.Id = "m"
+ l := ModelList{}
+ l.Put("m", m)
+ k, ok := l.At("m")
+ if !ok {
+ t.Error("want model back")
+ }
+ if got, want := k.Id, "m"; got != want {
+ t.Errorf("got %v want %v", got, want)
+ }
+}
+
+func TestModelList_Marshal(t *testing.T) {
+ l := ModelList{}
+ m := Model{Id: "myid"}
+ l.Put("myid", m)
+ data, err := json.Marshal(l)
+ if err != nil {
+ t.Error(err)
+ }
+ if got, want := string(data), `{"myid":{"id":"myid","properties":{}}}`; got != want {
+ t.Errorf("got %v want %v", got, want)
+ }
+}
+
+func TestModelList_Unmarshal(t *testing.T) {
+ data := `{"myid":{"id":"myid","properties":{}}}`
+ l := ModelList{}
+ if err := json.Unmarshal([]byte(data), &l); err != nil {
+ t.Error(err)
+ }
+ m, ok := l.At("myid")
+ if !ok {
+ t.Error("expected myid")
+ }
+ if got, want := m.Id, "myid"; got != want {
+ t.Errorf("got %v want %v", got, want)
+ }
+}
diff --git a/model_property_ext.go b/model_property_ext.go
new file mode 100644
index 0000000..a433b6b
--- /dev/null
+++ b/model_property_ext.go
@@ -0,0 +1,81 @@
+package swagger
+
+import (
+ "reflect"
+ "strings"
+)
+
+func (prop *ModelProperty) setDescription(field reflect.StructField) {
+ if tag := field.Tag.Get("description"); tag != "" {
+ prop.Description = tag
+ }
+}
+
+func (prop *ModelProperty) setDefaultValue(field reflect.StructField) {
+ if tag := field.Tag.Get("default"); tag != "" {
+ prop.DefaultValue = Special(tag)
+ }
+}
+
+func (prop *ModelProperty) setEnumValues(field reflect.StructField) {
+ // We use | to separate the enum values. This value is chosen
+ // since its unlikely to be useful in actual enumeration values.
+ if tag := field.Tag.Get("enum"); tag != "" {
+ prop.Enum = strings.Split(tag, "|")
+ }
+}
+
+func (prop *ModelProperty) setMaximum(field reflect.StructField) {
+ if tag := field.Tag.Get("maximum"); tag != "" {
+ prop.Maximum = tag
+ }
+}
+
+func (prop *ModelProperty) setType(field reflect.StructField) {
+ if tag := field.Tag.Get("type"); tag != "" {
+ // Check if the first two characters of the type tag are
+ // intended to emulate slice/array behaviour.
+ //
+ // If type is intended to be a slice/array then add the
+ // overriden type to the array item instead of the main property
+ if len(tag) > 2 && tag[0:2] == "[]" {
+ pType := "array"
+ prop.Type = &pType
+ prop.Items = new(Item)
+
+ iType := tag[2:]
+ prop.Items.Type = &iType
+ return
+ }
+
+ prop.Type = &tag
+ }
+}
+
+func (prop *ModelProperty) setMinimum(field reflect.StructField) {
+ if tag := field.Tag.Get("minimum"); tag != "" {
+ prop.Minimum = tag
+ }
+}
+
+func (prop *ModelProperty) setUniqueItems(field reflect.StructField) {
+ tag := field.Tag.Get("unique")
+ switch tag {
+ case "true":
+ v := true
+ prop.UniqueItems = &v
+ case "false":
+ v := false
+ prop.UniqueItems = &v
+ }
+}
+
+func (prop *ModelProperty) setPropertyMetadata(field reflect.StructField) {
+ prop.setDescription(field)
+ prop.setEnumValues(field)
+ prop.setMinimum(field)
+ prop.setMaximum(field)
+ prop.setUniqueItems(field)
+ prop.setDefaultValue(field)
+ prop.setType(field)
+}
diff --git a/model_property_ext_test.go b/model_property_ext_test.go
new file mode 100644
index 0000000..1123ed9
--- /dev/null
+++ b/model_property_ext_test.go
@@ -0,0 +1,70 @@
+package swagger
+
+import (
+ "net"
+ "testing"
+)
+
+// clear && go test -v -test.run TestThatExtraTagsAreReadIntoModel ...swagger
+func TestThatExtraTagsAreReadIntoModel(t *testing.T) {
+ type fakeint int
+ type fakearray string
+ type Anything struct {
+ Name string `description:"name" modelDescription:"a test"`
+ Size int `minimum:"0" maximum:"10"`
+ Stati string `enum:"off|on" default:"on" modelDescription:"more description"`
+ ID string `unique:"true"`
+ FakeInt fakeint `type:"integer"`
+ FakeArray fakearray `type:"[]string"`
+ IP net.IP `type:"string"`
+ Password string
+ }
+ m := modelsFromStruct(Anything{})
+ props, _ := m.At("swagger.Anything")
+ p1, _ := props.Properties.At("Name")
+ if got, want := p1.Description, "name"; got != want {
+ t.Errorf("got %v want %v", got, want)
+ }
+ p2, _ := props.Properties.At("Size")
+ if got, want := p2.Minimum, "0"; got != want {
+ t.Errorf("got %v want %v", got, want)
+ }
+ if got, want := p2.Maximum, "10"; got != want {
+ t.Errorf("got %v want %v", got, want)
+ }
+ p3, _ := props.Properties.At("Stati")
+ if got, want := p3.Enum[0], "off"; got != want {
+ t.Errorf("got %v want %v", got, want)
+ }
+ if got, want := p3.Enum[1], "on"; got != want {
+ t.Errorf("got %v want %v", got, want)
+ }
+ p4, _ := props.Properties.At("ID")
+ if got, want := *p4.UniqueItems, true; got != want {
+ t.Errorf("got %v want %v", got, want)
+ }
+ p5, _ := props.Properties.At("Password")
+ if got, want := *p5.Type, "string"; got != want {
+ t.Errorf("got %v want %v", got, want)
+ }
+ p6, _ := props.Properties.At("FakeInt")
+ if got, want := *p6.Type, "integer"; got != want {
+ t.Errorf("got %v want %v", got, want)
+ }
+ p7, _ := props.Properties.At("FakeArray")
+ if got, want := *p7.Type, "array"; got != want {
+ t.Errorf("got %v want %v", got, want)
+ }
+ p7p, _ := props.Properties.At("FakeArray")
+ if got, want := *p7p.Items.Type, "string"; got != want {
+ t.Errorf("got %v want %v", got, want)
+ }
+ p8, _ := props.Properties.At("IP")
+ if got, want := *p8.Type, "string"; got != want {
+ t.Errorf("got %v want %v", got, want)
+ }
+
+ if got, want := props.Description, "a test\nmore description"; got != want {
+ t.Errorf("got %v want %v", got, want)
+ }
+}
diff --git a/model_property_list.go b/model_property_list.go
new file mode 100644
index 0000000..3babb19
--- /dev/null
+++ b/model_property_list.go
@@ -0,0 +1,87 @@
+package swagger
+
+// Copyright 2015 Ernest Micklei. All rights reserved.
+// Use of this source code is governed by a license
+// that can be found in the LICENSE file.
+
+import (
+ "bytes"
+ "encoding/json"
+)
+
+// NamedModelProperty associates a name to a ModelProperty
+type NamedModelProperty struct {
+ Name string
+ Property ModelProperty
+}
+
+// ModelPropertyList encapsulates a list of NamedModelProperty (association)
+type ModelPropertyList struct {
+ List []NamedModelProperty
+}
+
+// At returns the ModelPropety by its name unless absent, then ok is false
+func (l *ModelPropertyList) At(name string) (p ModelProperty, ok bool) {
+ for _, each := range l.List {
+ if each.Name == name {
+ return each.Property, true
+ }
+ }
+ return p, false
+}
+
+// Put adds or replaces a ModelProperty with this name
+func (l *ModelPropertyList) Put(name string, prop ModelProperty) {
+ // maybe replace existing
+ for i, each := range l.List {
+ if each.Name == name {
+ // replace
+ l.List[i] = NamedModelProperty{Name: name, Property: prop}
+ return
+ }
+ }
+ // add
+ l.List = append(l.List, NamedModelProperty{Name: name, Property: prop})
+}
+
+// Do enumerates all the properties, each with its assigned name
+func (l *ModelPropertyList) Do(block func(name string, value ModelProperty)) {
+ for _, each := range l.List {
+ block(each.Name, each.Property)
+ }
+}
+
+// MarshalJSON writes the ModelPropertyList as if it was a map[string]ModelProperty
+func (l ModelPropertyList) MarshalJSON() ([]byte, error) {
+ var buf bytes.Buffer
+ encoder := json.NewEncoder(&buf)
+ buf.WriteString("{\n")
+ for i, each := range l.List {
+ buf.WriteString("\"")
+ buf.WriteString(each.Name)
+ buf.WriteString("\": ")
+ encoder.Encode(each.Property)
+ if i < len(l.List)-1 {
+ buf.WriteString(",\n")
+ }
+ }
+ buf.WriteString("}")
+ return buf.Bytes(), nil
+}
+
+// UnmarshalJSON reads back a ModelPropertyList. This is an expensive operation.
+func (l *ModelPropertyList) UnmarshalJSON(data []byte) error {
+ raw := map[string]interface{}{}
+ json.NewDecoder(bytes.NewReader(data)).Decode(&raw)
+ for k, v := range raw {
+ // produces JSON bytes for each value
+ data, err := json.Marshal(v)
+ if err != nil {
+ return err
+ }
+ var m ModelProperty
+ json.NewDecoder(bytes.NewReader(data)).Decode(&m)
+ l.Put(k, m)
+ }
+ return nil
+}
diff --git a/model_property_list_test.go b/model_property_list_test.go
new file mode 100644
index 0000000..2833ad8
--- /dev/null
+++ b/model_property_list_test.go
@@ -0,0 +1,47 @@
+package swagger
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestModelPropertyList(t *testing.T) {
+ l := ModelPropertyList{}
+ p := ModelProperty{Description: "d"}
+ l.Put("p", p)
+ q, ok := l.At("p")
+ if !ok {
+ t.Error("expected p")
+ }
+ if got, want := q.Description, "d"; got != want {
+ t.Errorf("got %v want %v", got, want)
+ }
+}
+
+func TestModelPropertyList_Marshal(t *testing.T) {
+ l := ModelPropertyList{}
+ p := ModelProperty{Description: "d"}
+ l.Put("p", p)
+ data, err := json.Marshal(l)
+ if err != nil {
+ t.Error(err)
+ }
+ if got, want := string(data), `{"p":{"description":"d"}}`; got != want {
+ t.Errorf("got %v want %v", got, want)
+ }
+}
+
+func TestModelPropertyList_Unmarshal(t *testing.T) {
+ data := `{"p":{"description":"d"}}`
+ l := ModelPropertyList{}
+ if err := json.Unmarshal([]byte(data), &l); err != nil {
+ t.Error(err)
+ }
+ m, ok := l.At("p")
+ if !ok {
+ t.Error("expected p")
+ }
+ if got, want := m.Description, "d"; got != want {
+ t.Errorf("got %v want %v", got, want)
+ }
+}
diff --git a/ordered_route_map.go b/ordered_route_map.go
new file mode 100644
index 0000000..b33ccfb
--- /dev/null
+++ b/ordered_route_map.go
@@ -0,0 +1,36 @@
+package swagger
+
+// Copyright 2015 Ernest Micklei. All rights reserved.
+// Use of this source code is governed by a license
+// that can be found in the LICENSE file.
+
+import "github.com/emicklei/go-restful"
+
+type orderedRouteMap struct {
+ elements map[string][]restful.Route
+ keys []string
+}
+
+func newOrderedRouteMap() *orderedRouteMap {
+ return &orderedRouteMap{
+ elements: map[string][]restful.Route{},
+ keys: []string{},
+ }
+}
+
+func (o *orderedRouteMap) Add(key string, route restful.Route) {
+ routes, ok := o.elements[key]
+ if ok {
+ routes = append(routes, route)
+ o.elements[key] = routes
+ return
+ }
+ o.elements[key] = []restful.Route{route}
+ o.keys = append(o.keys, key)
+}
+
+func (o *orderedRouteMap) Do(block func(key string, routes []restful.Route)) {
+ for _, k := range o.keys {
+ block(k, o.elements[k])
+ }
+}
diff --git a/ordered_route_map_test.go b/ordered_route_map_test.go
new file mode 100644
index 0000000..964e7da
--- /dev/null
+++ b/ordered_route_map_test.go
@@ -0,0 +1,29 @@
+package swagger
+
+import (
+ "testing"
+
+ "github.com/emicklei/go-restful"
+)
+
+// go test -v -test.run TestOrderedRouteMap ...swagger
+func TestOrderedRouteMap(t *testing.T) {
+ m := newOrderedRouteMap()
+ r1 := restful.Route{Path: "/r1"}
+ r2 := restful.Route{Path: "/r2"}
+ m.Add("a", r1)
+ m.Add("b", r2)
+ m.Add("b", r1)
+ m.Add("d", r2)
+ m.Add("c", r2)
+ order := ""
+ m.Do(func(k string, routes []restful.Route) {
+ order += k
+ if len(routes) == 0 {
+ t.Fail()
+ }
+ })
+ if order != "abdc" {
+ t.Fail()
+ }
+}
diff --git a/postbuild_model_test.go b/postbuild_model_test.go
new file mode 100644
index 0000000..3e20d2f
--- /dev/null
+++ b/postbuild_model_test.go
@@ -0,0 +1,42 @@
+package swagger
+
+import "testing"
+
+type Boat struct {
+ Length int `json:"-"` // on default, this makes the fields not required
+ Weight int `json:"-"`
+}
+
+// PostBuildModel is from swagger.ModelBuildable
+func (b Boat) PostBuildModel(m *Model) *Model {
+ // override required
+ m.Required = []string{"Length", "Weight"}
+
+ // add model property (just to test is can be added; is this a real usecase?)
+ extraType := "string"
+ m.Properties.Put("extra", ModelProperty{
+ Description: "extra description",
+ DataTypeFields: DataTypeFields{
+ Type: &extraType,
+ },
+ })
+ return m
+}
+
+func TestCustomPostModelBuilde(t *testing.T) {
+ testJsonFromStruct(t, Boat{}, `{
+ "swagger.Boat": {
+ "id": "swagger.Boat",
+ "required": [
+ "Length",
+ "Weight"
+ ],
+ "properties": {
+ "extra": {
+ "type": "string",
+ "description": "extra description"
+ }
+ }
+ }
+}`)
+}
diff --git a/swagger.go b/swagger.go
new file mode 100644
index 0000000..9c40833
--- /dev/null
+++ b/swagger.go
@@ -0,0 +1,185 @@
+// Package swagger implements the structures of the Swagger
+// https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md
+package swagger
+
+const swaggerVersion = "1.2"
+
+// 4.3.3 Data Type Fields
+type DataTypeFields struct {
+ Type *string `json:"type,omitempty"` // if Ref not used
+ Ref *string `json:"$ref,omitempty"` // if Type not used
+ Format string `json:"format,omitempty"`
+ DefaultValue Special `json:"defaultValue,omitempty"`
+ Enum []string `json:"enum,omitempty"`
+ Minimum string `json:"minimum,omitempty"`
+ Maximum string `json:"maximum,omitempty"`
+ Items *Item `json:"items,omitempty"`
+ UniqueItems *bool `json:"uniqueItems,omitempty"`
+}
+
+type Special string
+
+// 4.3.4 Items Object
+type Item struct {
+ Type *string `json:"type,omitempty"`
+ Ref *string `json:"$ref,omitempty"`
+ Format string `json:"format,omitempty"`
+}
+
+// 5.1 Resource Listing
+type ResourceListing struct {
+ SwaggerVersion string `json:"swaggerVersion"` // e.g 1.2
+ Apis []Resource `json:"apis"`
+ ApiVersion string `json:"apiVersion"`
+ Info Info `json:"info"`
+ Authorizations []Authorization `json:"authorizations,omitempty"`
+}
+
+// 5.1.2 Resource Object
+type Resource struct {
+ Path string `json:"path"` // relative or absolute, must start with /
+ Description string `json:"description"`
+}
+
+// 5.1.3 Info Object
+type Info struct {
+ Title string `json:"title"`
+ Description string `json:"description"`
+ TermsOfServiceUrl string `json:"termsOfServiceUrl,omitempty"`
+ Contact string `json:"contact,omitempty"`
+ License string `json:"license,omitempty"`
+ LicenseUrl string `json:"licenseUrl,omitempty"`
+}
+
+// 5.1.5
+type Authorization struct {
+ Type string `json:"type"`
+ PassAs string `json:"passAs"`
+ Keyname string `json:"keyname"`
+ Scopes []Scope `json:"scopes"`
+ GrantTypes []GrantType `json:"grandTypes"`
+}
+
+// 5.1.6, 5.2.11
+type Scope struct {
+ // Required. The name of the scope.
+ Scope string `json:"scope"`
+ // Recommended. A short description of the scope.
+ Description string `json:"description"`
+}
+
+// 5.1.7
+type GrantType struct {
+ Implicit Implicit `json:"implicit"`
+ AuthorizationCode AuthorizationCode `json:"authorization_code"`
+}
+
+// 5.1.8 Implicit Object
+type Implicit struct {
+ // Required. The login endpoint definition.
+ loginEndpoint LoginEndpoint `json:"loginEndpoint"`
+ // An optional alternative name to standard "access_token" OAuth2 parameter.
+ TokenName string `json:"tokenName"`
+}
+
+// 5.1.9 Authorization Code Object
+type AuthorizationCode struct {
+ TokenRequestEndpoint TokenRequestEndpoint `json:"tokenRequestEndpoint"`
+ TokenEndpoint TokenEndpoint `json:"tokenEndpoint"`
+}
+
+// 5.1.10 Login Endpoint Object
+type LoginEndpoint struct {
+ // Required. The URL of the authorization endpoint for the implicit grant flow. The value SHOULD be in a URL format.
+ Url string `json:"url"`
+}
+
+// 5.1.11 Token Request Endpoint Object
+type TokenRequestEndpoint struct {
+ // Required. The URL of the authorization endpoint for the authentication code grant flow. The value SHOULD be in a URL format.
+ Url string `json:"url"`
+ // An optional alternative name to standard "client_id" OAuth2 parameter.
+ ClientIdName string `json:"clientIdName"`
+ // An optional alternative name to the standard "client_secret" OAuth2 parameter.
+ ClientSecretName string `json:"clientSecretName"`
+}
+
+// 5.1.12 Token Endpoint Object
+type TokenEndpoint struct {
+ // Required. The URL of the token endpoint for the authentication code grant flow. The value SHOULD be in a URL format.
+ Url string `json:"url"`
+ // An optional alternative name to standard "access_token" OAuth2 parameter.
+ TokenName string `json:"tokenName"`
+}
+
+// 5.2 API Declaration
+type ApiDeclaration struct {
+ SwaggerVersion string `json:"swaggerVersion"`
+ ApiVersion string `json:"apiVersion"`
+ BasePath string `json:"basePath"`
+ ResourcePath string `json:"resourcePath"` // must start with /
+ Info Info `json:"info"`
+ Apis []Api `json:"apis,omitempty"`
+ Models ModelList `json:"models,omitempty"`
+ Produces []string `json:"produces,omitempty"`
+ Consumes []string `json:"consumes,omitempty"`
+ Authorizations []Authorization `json:"authorizations,omitempty"`
+}
+
+// 5.2.2 API Object
+type Api struct {
+ Path string `json:"path"` // relative or absolute, must start with /
+ Description string `json:"description"`
+ Operations []Operation `json:"operations,omitempty"`
+}
+
+// 5.2.3 Operation Object
+type Operation struct {
+ DataTypeFields
+ Method string `json:"method"`
+ Summary string `json:"summary,omitempty"`
+ Notes string `json:"notes,omitempty"`
+ Nickname string `json:"nickname"`
+ Authorizations []Authorization `json:"authorizations,omitempty"`
+ Parameters []Parameter `json:"parameters"`
+ ResponseMessages []ResponseMessage `json:"responseMessages,omitempty"` // optional
+ Produces []string `json:"produces,omitempty"`
+ Consumes []string `json:"consumes,omitempty"`
+ Deprecated string `json:"deprecated,omitempty"`
+}
+
+// 5.2.4 Parameter Object
+type Parameter struct {
+ DataTypeFields
+ ParamType string `json:"paramType"` // path,query,body,header,form
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Required bool `json:"required"`
+ AllowMultiple bool `json:"allowMultiple"`
+}
+
+// 5.2.5 Response Message Object
+type ResponseMessage struct {
+ Code int `json:"code"`
+ Message string `json:"message"`
+ ResponseModel string `json:"responseModel,omitempty"`
+}
+
+// 5.2.6, 5.2.7 Models Object
+type Model struct {
+ Id string `json:"id"`
+ Description string `json:"description,omitempty"`
+ Required []string `json:"required,omitempty"`
+ Properties ModelPropertyList `json:"properties"`
+ SubTypes []string `json:"subTypes,omitempty"`
+ Discriminator string `json:"discriminator,omitempty"`
+}
+
+// 5.2.8 Properties Object
+type ModelProperty struct {
+ DataTypeFields
+ Description string `json:"description,omitempty"`
+}
+
+// 5.2.10
+type Authorizations map[string]Authorization
diff --git a/swagger_builder.go b/swagger_builder.go
new file mode 100644
index 0000000..05a3c7e
--- /dev/null
+++ b/swagger_builder.go
@@ -0,0 +1,21 @@
+package swagger
+
+type SwaggerBuilder struct {
+ SwaggerService
+}
+
+func NewSwaggerBuilder(config Config) *SwaggerBuilder {
+ return &SwaggerBuilder{*newSwaggerService(config)}
+}
+
+func (sb SwaggerBuilder) ProduceListing() ResourceListing {
+ return sb.SwaggerService.produceListing()
+}
+
+func (sb SwaggerBuilder) ProduceAllDeclarations() map[string]ApiDeclaration {
+ return sb.SwaggerService.produceAllDeclarations()
+}
+
+func (sb SwaggerBuilder) ProduceDeclarations(route string) (*ApiDeclaration, bool) {
+ return sb.SwaggerService.produceDeclarations(route)
+}
diff --git a/swagger_test.go b/swagger_test.go
new file mode 100644
index 0000000..3db904a
--- /dev/null
+++ b/swagger_test.go
@@ -0,0 +1,318 @@
+package swagger
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/emicklei/go-restful"
+ "github.com/emicklei/go-restful-swagger12/test_package"
+)
+
+func TestInfoStruct_Issue231(t *testing.T) {
+ config := Config{
+ Info: Info{
+ Title: "Title",
+ Description: "Description",
+ TermsOfServiceUrl: "http://example.com",
+ Contact: "example@example.com",
+ License: "License",
+ LicenseUrl: "http://example.com/license.txt",
+ },
+ }
+ sws := newSwaggerService(config)
+ str, err := json.MarshalIndent(sws.produceListing(), "", " ")
+ if err != nil {
+ t.Fatal(err)
+ }
+ compareJson(t, string(str), `
+ {
+ "apiVersion": "",
+ "swaggerVersion": "1.2",
+ "apis": null,
+ "info": {
+ "title": "Title",
+ "description": "Description",
+ "termsOfServiceUrl": "http://example.com",
+ "contact": "example@example.com",
+ "license": "License",
+ "licenseUrl": "http://example.com/license.txt"
+ }
+ }
+ `)
+}
+
+// go test -v -test.run TestThatMultiplePathsOnRootAreHandled ...swagger
+func TestThatMultiplePathsOnRootAreHandled(t *testing.T) {
+ ws1 := new(restful.WebService)
+ ws1.Route(ws1.GET("/_ping").To(dummy))
+ ws1.Route(ws1.GET("/version").To(dummy))
+
+ cfg := Config{
+ WebServicesUrl: "http://here.com",
+ ApiPath: "/apipath",
+ WebServices: []*restful.WebService{ws1},
+ }
+ sws := newSwaggerService(cfg)
+ decl := sws.composeDeclaration(ws1, "/")
+ if got, want := len(decl.Apis), 2; got != want {
+ t.Errorf("got %v want %v", got, want)
+ }
+}
+
+func TestWriteSamples(t *testing.T) {
+ ws1 := new(restful.WebService)
+ ws1.Route(ws1.GET("/object").To(dummy).Writes(test_package.TestStruct{}))
+ ws1.Route(ws1.GET("/array").To(dummy).Writes([]test_package.TestStruct{}))
+ ws1.Route(ws1.GET("/object_and_array").To(dummy).Writes(struct{ Abc test_package.TestStruct }{}))
+
+ cfg := Config{
+ WebServicesUrl: "http://here.com",
+ ApiPath: "/apipath",
+ WebServices: []*restful.WebService{ws1},
+ }
+ sws := newSwaggerService(cfg)
+
+ decl := sws.composeDeclaration(ws1, "/")
+
+ str, err := json.MarshalIndent(decl.Apis, "", " ")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ compareJson(t, string(str), `
+ [
+ {
+ "path": "/object",
+ "description": "",
+ "operations": [
+ {
+ "type": "test_package.TestStruct",
+ "method": "GET",
+ "nickname": "dummy",
+ "parameters": []
+ }
+ ]
+ },
+ {
+ "path": "/array",
+ "description": "",
+ "operations": [
+ {
+ "type": "array",
+ "items": {
+ "$ref": "test_package.TestStruct"
+ },
+ "method": "GET",
+ "nickname": "dummy",
+ "parameters": []
+ }
+ ]
+ },
+ {
+ "path": "/object_and_array",
+ "description": "",
+ "operations": [
+ {
+ "type": "struct { Abc test_package.TestStruct }",
+ "method": "GET",
+ "nickname": "dummy",
+ "parameters": []
+ }
+ ]
+ }
+ ]`)
+
+ str, err = json.MarshalIndent(decl.Models, "", " ")
+ if err != nil {
+ t.Fatal(err)
+ }
+ compareJson(t, string(str), `
+ {
+ "test_package.TestStruct": {
+ "id": "test_package.TestStruct",
+ "required": [
+ "TestField"
+ ],
+ "properties": {
+ "TestField": {
+ "type": "string"
+ }
+ }
+ },
+ "||test_package.TestStruct": {
+ "id": "||test_package.TestStruct",
+ "properties": {}
+ },
+ "struct { Abc test_package.TestStruct }": {
+ "id": "struct { Abc test_package.TestStruct }",
+ "required": [
+ "Abc"
+ ],
+ "properties": {
+ "Abc": {
+ "$ref": "test_package.TestStruct"
+ }
+ }
+ }
+ }`)
+}
+
+func TestRoutesWithCommonPart(t *testing.T) {
+ ws1 := new(restful.WebService)
+ ws1.Path("/")
+ ws1.Route(ws1.GET("/foobar").To(dummy).Writes(test_package.TestStruct{}))
+ ws1.Route(ws1.HEAD("/foobar").To(dummy).Writes(test_package.TestStruct{}))
+ ws1.Route(ws1.GET("/foo").To(dummy).Writes([]test_package.TestStruct{}))
+ ws1.Route(ws1.HEAD("/foo").To(dummy).Writes(test_package.TestStruct{}))
+
+ cfg := Config{
+ WebServicesUrl: "http://here.com",
+ ApiPath: "/apipath",
+ WebServices: []*restful.WebService{ws1},
+ }
+ sws := newSwaggerService(cfg)
+
+ decl := sws.composeDeclaration(ws1, "/foo")
+
+ str, err := json.MarshalIndent(decl.Apis, "", " ")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ compareJson(t, string(str), `[
+ {
+ "path": "/foo",
+ "description": "",
+ "operations": [
+ {
+ "type": "array",
+ "items": {
+ "$ref": "test_package.TestStruct"
+ },
+ "method": "GET",
+ "nickname": "dummy",
+ "parameters": []
+ },
+ {
+ "type": "test_package.TestStruct",
+ "method": "HEAD",
+ "nickname": "dummy",
+ "parameters": []
+ }
+ ]
+ }
+ ]`)
+}
+
+// go test -v -test.run TestServiceToApi ...swagger
+func TestServiceToApi(t *testing.T) {
+ ws := new(restful.WebService)
+ ws.Path("/tests")
+ ws.Consumes(restful.MIME_JSON)
+ ws.Produces(restful.MIME_XML)
+ ws.Route(ws.GET("/a").To(dummy).Writes(sample{}))
+ ws.Route(ws.PUT("/b").To(dummy).Writes(sample{}))
+ ws.Route(ws.POST("/c").To(dummy).Writes(sample{}))
+ ws.Route(ws.DELETE("/d").To(dummy).Writes(sample{}))
+
+ ws.Route(ws.GET("/d").To(dummy).Writes(sample{}))
+ ws.Route(ws.PUT("/c").To(dummy).Writes(sample{}))
+ ws.Route(ws.POST("/b").To(dummy).Writes(sample{}))
+ ws.Route(ws.DELETE("/a").To(dummy).Writes(sample{}))
+ ws.ApiVersion("1.2.3")
+ cfg := Config{
+ WebServicesUrl: "http://here.com",
+ ApiPath: "/apipath",
+ WebServices: []*restful.WebService{ws},
+ PostBuildHandler: func(in *ApiDeclarationList) {},
+ }
+ sws := newSwaggerService(cfg)
+ decl := sws.composeDeclaration(ws, "/tests")
+ // checks
+ if decl.ApiVersion != "1.2.3" {
+ t.Errorf("got %v want %v", decl.ApiVersion, "1.2.3")
+ }
+ if decl.BasePath != "http://here.com" {
+ t.Errorf("got %v want %v", decl.BasePath, "http://here.com")
+ }
+ if len(decl.Apis) != 4 {
+ t.Errorf("got %v want %v", len(decl.Apis), 4)
+ }
+ pathOrder := ""
+ for _, each := range decl.Apis {
+ pathOrder += each.Path
+ for _, other := range each.Operations {
+ pathOrder += other.Method
+ }
+ }
+
+ if pathOrder != "/tests/aGETDELETE/tests/bPUTPOST/tests/cPOSTPUT/tests/dDELETEGET" {
+ t.Errorf("got %v want %v", pathOrder, "see test source")
+ }
+}
+
+func dummy(i *restful.Request, o *restful.Response) {}
+
+// go test -v -test.run TestIssue78 ...swagger
+type Response struct {
+ Code int
+ Users *[]User
+ Items *[]TestItem
+}
+type User struct {
+ Id, Name string
+}
+type TestItem struct {
+ Id, Name string
+}
+
+// clear && go test -v -test.run TestComposeResponseMessages ...swagger
+func TestComposeResponseMessages(t *testing.T) {
+ responseErrors := map[int]restful.ResponseError{}
+ responseErrors[400] = restful.ResponseError{Code: 400, Message: "Bad Request", Model: TestItem{}}
+ route := restful.Route{ResponseErrors: responseErrors}
+ decl := new(ApiDeclaration)
+ decl.Models = ModelList{}
+ msgs := composeResponseMessages(route, decl, &Config{})
+ if msgs[0].ResponseModel != "swagger.TestItem" {
+ t.Errorf("got %s want swagger.TestItem", msgs[0].ResponseModel)
+ }
+}
+
+func TestIssue78(t *testing.T) {
+ sws := newSwaggerService(Config{})
+ models := new(ModelList)
+ sws.addModelFromSampleTo(&Operation{}, true, Response{Items: &[]TestItem{}}, models)
+ model, ok := models.At("swagger.Response")
+ if !ok {
+ t.Fatal("missing response model")
+ }
+ if "swagger.Response" != model.Id {
+ t.Fatal("wrong model id:" + model.Id)
+ }
+ code, ok := model.Properties.At("Code")
+ if !ok {
+ t.Fatal("missing code")
+ }
+ if "integer" != *code.Type {
+ t.Fatal("wrong code type:" + *code.Type)
+ }
+ items, ok := model.Properties.At("Items")
+ if !ok {
+ t.Fatal("missing items")
+ }
+ if "array" != *items.Type {
+ t.Fatal("wrong items type:" + *items.Type)
+ }
+ items_items := items.Items
+ if items_items == nil {
+ t.Fatal("missing items->items")
+ }
+ ref := items_items.Ref
+ if ref == nil {
+ t.Fatal("missing $ref")
+ }
+ if *ref != "swagger.TestItem" {
+ t.Fatal("wrong $ref:" + *ref)
+ }
+}
diff --git a/swagger_webservice.go b/swagger_webservice.go
new file mode 100644
index 0000000..d906231
--- /dev/null
+++ b/swagger_webservice.go
@@ -0,0 +1,443 @@
+package swagger
+
+import (
+ "fmt"
+
+ "github.com/emicklei/go-restful"
+ // "github.com/emicklei/hopwatch"
+ "net/http"
+ "reflect"
+ "sort"
+ "strings"
+
+ "github.com/emicklei/go-restful/log"
+)
+
+type SwaggerService struct {
+ config Config
+ apiDeclarationMap *ApiDeclarationList
+}
+
+func newSwaggerService(config Config) *SwaggerService {
+ sws := &SwaggerService{
+ config: config,
+ apiDeclarationMap: new(ApiDeclarationList)}
+
+ // Build all ApiDeclarations
+ for _, each := range config.WebServices {
+ rootPath := each.RootPath()
+ // skip the api service itself
+ if rootPath != config.ApiPath {
+ if rootPath == "" || rootPath == "/" {
+ // use routes
+ for _, route := range each.Routes() {
+ entry := staticPathFromRoute(route)
+ _, exists := sws.apiDeclarationMap.At(entry)
+ if !exists {
+ sws.apiDeclarationMap.Put(entry, sws.composeDeclaration(each, entry))
+ }
+ }
+ } else { // use root path
+ sws.apiDeclarationMap.Put(each.RootPath(), sws.composeDeclaration(each, each.RootPath()))
+ }
+ }
+ }
+
+ // if specified then call the PostBuilderHandler
+ if config.PostBuildHandler != nil {
+ config.PostBuildHandler(sws.apiDeclarationMap)
+ }
+ return sws
+}
+
+// LogInfo is the function that is called when this package needs to log. It defaults to log.Printf
+var LogInfo = func(format string, v ...interface{}) {
+ // use the restful package-wide logger
+ log.Printf(format, v...)
+}
+
+// InstallSwaggerService add the WebService that provides the API documentation of all services
+// conform the Swagger documentation specifcation. (https://github.com/wordnik/swagger-core/wiki).
+func InstallSwaggerService(aSwaggerConfig Config) {
+ RegisterSwaggerService(aSwaggerConfig, restful.DefaultContainer)
+}
+
+// RegisterSwaggerService add the WebService that provides the API documentation of all services
+// conform the Swagger documentation specifcation. (https://github.com/wordnik/swagger-core/wiki).
+func RegisterSwaggerService(config Config, wsContainer *restful.Container) {
+ sws := newSwaggerService(config)
+ ws := new(restful.WebService)
+ ws.Path(config.ApiPath)
+ ws.Produces(restful.MIME_JSON)
+ if config.DisableCORS {
+ ws.Filter(enableCORS)
+ }
+ ws.Route(ws.GET("/").To(sws.getListing))
+ ws.Route(ws.GET("/{a}").To(sws.getDeclarations))
+ ws.Route(ws.GET("/{a}/{b}").To(sws.getDeclarations))
+ ws.Route(ws.GET("/{a}/{b}/{c}").To(sws.getDeclarations))
+ ws.Route(ws.GET("/{a}/{b}/{c}/{d}").To(sws.getDeclarations))
+ ws.Route(ws.GET("/{a}/{b}/{c}/{d}/{e}").To(sws.getDeclarations))
+ ws.Route(ws.GET("/{a}/{b}/{c}/{d}/{e}/{f}").To(sws.getDeclarations))
+ ws.Route(ws.GET("/{a}/{b}/{c}/{d}/{e}/{f}/{g}").To(sws.getDeclarations))
+ LogInfo("[restful/swagger] listing is available at %v%v", config.WebServicesUrl, config.ApiPath)
+ wsContainer.Add(ws)
+
+ // Check paths for UI serving
+ if config.StaticHandler == nil && config.SwaggerFilePath != "" && config.SwaggerPath != "" {
+ swaggerPathSlash := config.SwaggerPath
+ // path must end with slash /
+ if "/" != config.SwaggerPath[len(config.SwaggerPath)-1:] {
+ LogInfo("[restful/swagger] use corrected SwaggerPath ; must end with slash (/)")
+ swaggerPathSlash += "/"
+ }
+
+ LogInfo("[restful/swagger] %v%v is mapped to folder %v", config.WebServicesUrl, swaggerPathSlash, config.SwaggerFilePath)
+ wsContainer.Handle(swaggerPathSlash, http.StripPrefix(swaggerPathSlash, http.FileServer(http.Dir(config.SwaggerFilePath))))
+
+ //if we define a custom static handler use it
+ } else if config.StaticHandler != nil && config.SwaggerPath != "" {
+ swaggerPathSlash := config.SwaggerPath
+ // path must end with slash /
+ if "/" != config.SwaggerPath[len(config.SwaggerPath)-1:] {
+ LogInfo("[restful/swagger] use corrected SwaggerFilePath ; must end with slash (/)")
+ swaggerPathSlash += "/"
+
+ }
+ LogInfo("[restful/swagger] %v%v is mapped to custom Handler %T", config.WebServicesUrl, swaggerPathSlash, config.StaticHandler)
+ wsContainer.Handle(swaggerPathSlash, config.StaticHandler)
+
+ } else {
+ LogInfo("[restful/swagger] Swagger(File)Path is empty ; no UI is served")
+ }
+}
+
+func staticPathFromRoute(r restful.Route) string {
+ static := r.Path
+ bracket := strings.Index(static, "{")
+ if bracket <= 1 { // result cannot be empty
+ return static
+ }
+ if bracket != -1 {
+ static = r.Path[:bracket]
+ }
+ if strings.HasSuffix(static, "/") {
+ return static[:len(static)-1]
+ } else {
+ return static
+ }
+}
+
+func enableCORS(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
+ if origin := req.HeaderParameter(restful.HEADER_Origin); origin != "" {
+ // prevent duplicate header
+ if len(resp.Header().Get(restful.HEADER_AccessControlAllowOrigin)) == 0 {
+ resp.AddHeader(restful.HEADER_AccessControlAllowOrigin, origin)
+ }
+ }
+ chain.ProcessFilter(req, resp)
+}
+
+func (sws SwaggerService) getListing(req *restful.Request, resp *restful.Response) {
+ listing := sws.produceListing()
+ resp.WriteAsJson(listing)
+}
+
+func (sws SwaggerService) produceListing() ResourceListing {
+ listing := ResourceListing{SwaggerVersion: swaggerVersion, ApiVersion: sws.config.ApiVersion, Info: sws.config.Info}
+ sws.apiDeclarationMap.Do(func(k string, v ApiDeclaration) {
+ ref := Resource{Path: k}
+ if len(v.Apis) > 0 { // use description of first (could still be empty)
+ ref.Description = v.Apis[0].Description
+ }
+ listing.Apis = append(listing.Apis, ref)
+ })
+ return listing
+}
+
+func (sws SwaggerService) getDeclarations(req *restful.Request, resp *restful.Response) {
+ decl, ok := sws.produceDeclarations(composeRootPath(req))
+ if !ok {
+ resp.WriteErrorString(http.StatusNotFound, "ApiDeclaration not found")
+ return
+ }
+ // unless WebServicesUrl is given
+ if len(sws.config.WebServicesUrl) == 0 {
+ // update base path from the actual request
+ // TODO how to detect https? assume http for now
+ var host string
+ // X-Forwarded-Host or Host or Request.Host
+ hostvalues, ok := req.Request.Header["X-Forwarded-Host"] // apache specific?
+ if !ok || len(hostvalues) == 0 {
+ forwarded, ok := req.Request.Header["Host"] // without reverse-proxy
+ if !ok || len(forwarded) == 0 {
+ // fallback to Host field
+ host = req.Request.Host
+ } else {
+ host = forwarded[0]
+ }
+ } else {
+ host = hostvalues[0]
+ }
+ // inspect Referer for the scheme (http vs https)
+ scheme := "http"
+ if referer := req.Request.Header["Referer"]; len(referer) > 0 {
+ if strings.HasPrefix(referer[0], "https") {
+ scheme = "https"
+ }
+ }
+ decl.BasePath = fmt.Sprintf("%s://%s", scheme, host)
+ }
+ resp.WriteAsJson(decl)
+}
+
+func (sws SwaggerService) produceAllDeclarations() map[string]ApiDeclaration {
+ decls := map[string]ApiDeclaration{}
+ sws.apiDeclarationMap.Do(func(k string, v ApiDeclaration) {
+ decls[k] = v
+ })
+ return decls
+}
+
+func (sws SwaggerService) produceDeclarations(route string) (*ApiDeclaration, bool) {
+ decl, ok := sws.apiDeclarationMap.At(route)
+ if !ok {
+ return nil, false
+ }
+ decl.BasePath = sws.config.WebServicesUrl
+ return &decl, true
+}
+
+// composeDeclaration uses all routes and parameters to create a ApiDeclaration
+func (sws SwaggerService) composeDeclaration(ws *restful.WebService, pathPrefix string) ApiDeclaration {
+ decl := ApiDeclaration{
+ SwaggerVersion: swaggerVersion,
+ BasePath: sws.config.WebServicesUrl,
+ ResourcePath: pathPrefix,
+ Models: ModelList{},
+ ApiVersion: ws.Version()}
+
+ // collect any path parameters
+ rootParams := []Parameter{}
+ for _, param := range ws.PathParameters() {
+ rootParams = append(rootParams, asSwaggerParameter(param.Data()))
+ }
+ // aggregate by path
+ pathToRoutes := newOrderedRouteMap()
+ for _, other := range ws.Routes() {
+ if strings.HasPrefix(other.Path, pathPrefix) {
+ if len(pathPrefix) > 1 && len(other.Path) > len(pathPrefix) && other.Path[len(pathPrefix)] != '/' {
+ continue
+ }
+ pathToRoutes.Add(other.Path, other)
+ }
+ }
+ pathToRoutes.Do(func(path string, routes []restful.Route) {
+ api := Api{Path: strings.TrimSuffix(withoutWildcard(path), "/"), Description: ws.Documentation()}
+ voidString := "void"
+ for _, route := range routes {
+ operation := Operation{
+ Method: route.Method,
+ Summary: route.Doc,
+ Notes: route.Notes,
+ // Type gets overwritten if there is a write sample
+ DataTypeFields: DataTypeFields{Type: &voidString},
+ Parameters: []Parameter{},
+ Nickname: route.Operation,
+ ResponseMessages: composeResponseMessages(route, &decl, &sws.config)}
+
+ operation.Consumes = route.Consumes
+ operation.Produces = route.Produces
+
+ // share root params if any
+ for _, swparam := range rootParams {
+ operation.Parameters = append(operation.Parameters, swparam)
+ }
+ // route specific params
+ for _, param := range route.ParameterDocs {
+ operation.Parameters = append(operation.Parameters, asSwaggerParameter(param.Data()))
+ }
+
+ sws.addModelsFromRouteTo(&operation, route, &decl)
+ api.Operations = append(api.Operations, operation)
+ }
+ decl.Apis = append(decl.Apis, api)
+ })
+ return decl
+}
+
+func withoutWildcard(path string) string {
+ if strings.HasSuffix(path, ":*}") {
+ return path[0:len(path)-3] + "}"
+ }
+ return path
+}
+
+// composeResponseMessages takes the ResponseErrors (if any) and creates ResponseMessages from them.
+func composeResponseMessages(route restful.Route, decl *ApiDeclaration, config *Config) (messages []ResponseMessage) {
+ if route.ResponseErrors == nil {
+ return messages
+ }
+ // sort by code
+ codes := sort.IntSlice{}
+ for code := range route.ResponseErrors {
+ codes = append(codes, code)
+ }
+ codes.Sort()
+ for _, code := range codes {
+ each := route.ResponseErrors[code]
+ message := ResponseMessage{
+ Code: code,
+ Message: each.Message,
+ }
+ if each.Model != nil {
+ st := reflect.TypeOf(each.Model)
+ isCollection, st := detectCollectionType(st)
+ // collection cannot be in responsemodel
+ if !isCollection {
+ modelName := modelBuilder{}.keyFrom(st)
+ modelBuilder{Models: &decl.Models, Config: config}.addModel(st, "")
+ message.ResponseModel = modelName
+ }
+ }
+ messages = append(messages, message)
+ }
+ return
+}
+
+// addModelsFromRoute takes any read or write sample from the Route and creates a Swagger model from it.
+func (sws SwaggerService) addModelsFromRouteTo(operation *Operation, route restful.Route, decl *ApiDeclaration) {
+ if route.ReadSample != nil {
+ sws.addModelFromSampleTo(operation, false, route.ReadSample, &decl.Models)
+ }
+ if route.WriteSample != nil {
+ sws.addModelFromSampleTo(operation, true, route.WriteSample, &decl.Models)
+ }
+}
+
+func detectCollectionType(st reflect.Type) (bool, reflect.Type) {
+ isCollection := false
+ if st.Kind() == reflect.Slice || st.Kind() == reflect.Array {
+ st = st.Elem()
+ isCollection = true
+ } else {
+ if st.Kind() == reflect.Ptr {
+ if st.Elem().Kind() == reflect.Slice || st.Elem().Kind() == reflect.Array {
+ st = st.Elem().Elem()
+ isCollection = true
+ }
+ }
+ }
+ return isCollection, st
+}
+
+// addModelFromSample creates and adds (or overwrites) a Model from a sample resource
+func (sws SwaggerService) addModelFromSampleTo(operation *Operation, isResponse bool, sample interface{}, models *ModelList) {
+ mb := modelBuilder{Models: models, Config: &sws.config}
+ if isResponse {
+ sampleType, items := asDataType(sample, &sws.config)
+ operation.Type = sampleType
+ operation.Items = items
+ }
+ mb.addModelFrom(sample)
+}
+
+func asSwaggerParameter(param restful.ParameterData) Parameter {
+ return Parameter{
+ DataTypeFields: DataTypeFields{
+ Type: &param.DataType,
+ Format: asFormat(param.DataType, param.DataFormat),
+ DefaultValue: Special(param.DefaultValue),
+ },
+ Name: param.Name,
+ Description: param.Description,
+ ParamType: asParamType(param.Kind),
+
+ Required: param.Required}
+}
+
+// Between 1..7 path parameters is supported
+func composeRootPath(req *restful.Request) string {
+ path := "/" + req.PathParameter("a")
+ b := req.PathParameter("b")
+ if b == "" {
+ return path
+ }
+ path = path + "/" + b
+ c := req.PathParameter("c")
+ if c == "" {
+ return path
+ }
+ path = path + "/" + c
+ d := req.PathParameter("d")
+ if d == "" {
+ return path
+ }
+ path = path + "/" + d
+ e := req.PathParameter("e")
+ if e == "" {
+ return path
+ }
+ path = path + "/" + e
+ f := req.PathParameter("f")
+ if f == "" {
+ return path
+ }
+ path = path + "/" + f
+ g := req.PathParameter("g")
+ if g == "" {
+ return path
+ }
+ return path + "/" + g
+}
+
+func asFormat(dataType string, dataFormat string) string {
+ if dataFormat != "" {
+ return dataFormat
+ }
+ return "" // TODO
+}
+
+func asParamType(kind int) string {
+ switch {
+ case kind == restful.PathParameterKind:
+ return "path"
+ case kind == restful.QueryParameterKind:
+ return "query"
+ case kind == restful.BodyParameterKind:
+ return "body"
+ case kind == restful.HeaderParameterKind:
+ return "header"
+ case kind == restful.FormParameterKind:
+ return "form"
+ }
+ return ""
+}
+
+func asDataType(any interface{}, config *Config) (*string, *Item) {
+ // If it's not a collection, return the suggested model name
+ st := reflect.TypeOf(any)
+ isCollection, st := detectCollectionType(st)
+ modelName := modelBuilder{}.keyFrom(st)
+ // if it's not a collection we are done
+ if !isCollection {
+ return &modelName, nil
+ }
+
+ // XXX: This is not very elegant
+ // We create an Item object referring to the given model
+ models := ModelList{}
+ mb := modelBuilder{Models: &models, Config: config}
+ mb.addModelFrom(any)
+
+ elemTypeName := mb.getElementTypeName(modelName, "", st)
+ item := new(Item)
+ if mb.isPrimitiveType(elemTypeName) {
+ mapped := mb.jsonSchemaType(elemTypeName)
+ item.Type = &mapped
+ } else {
+ item.Ref = &elemTypeName
+ }
+ tmp := "array"
+ return &tmp, item
+}
diff --git a/test_package/struct.go b/test_package/struct.go
new file mode 100644
index 0000000..b9a6f93
--- /dev/null
+++ b/test_package/struct.go
@@ -0,0 +1,5 @@
+package test_package
+
+type TestStruct struct {
+ TestField string
+}
diff --git a/utils_test.go b/utils_test.go
new file mode 100644
index 0000000..220289a
--- /dev/null
+++ b/utils_test.go
@@ -0,0 +1,86 @@
+package swagger
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "strings"
+ "testing"
+)
+
+func testJsonFromStructWithConfig(t *testing.T, sample interface{}, expectedJson string, config *Config) bool {
+ m := modelsFromStructWithConfig(sample, config)
+ data, _ := json.MarshalIndent(m, " ", " ")
+ return compareJson(t, string(data), expectedJson)
+}
+
+func modelsFromStructWithConfig(sample interface{}, config *Config) *ModelList {
+ models := new(ModelList)
+ builder := modelBuilder{Models: models, Config: config}
+ builder.addModelFrom(sample)
+ return models
+}
+
+func testJsonFromStruct(t *testing.T, sample interface{}, expectedJson string) bool {
+ return testJsonFromStructWithConfig(t, sample, expectedJson, &Config{})
+}
+
+func modelsFromStruct(sample interface{}) *ModelList {
+ return modelsFromStructWithConfig(sample, &Config{})
+}
+
+func compareJson(t *testing.T, actualJsonAsString string, expectedJsonAsString string) bool {
+ success := false
+ var actualMap map[string]interface{}
+ json.Unmarshal([]byte(actualJsonAsString), &actualMap)
+ var expectedMap map[string]interface{}
+ err := json.Unmarshal([]byte(expectedJsonAsString), &expectedMap)
+ if err != nil {
+ var actualArray []interface{}
+ json.Unmarshal([]byte(actualJsonAsString), &actualArray)
+ var expectedArray []interface{}
+ err := json.Unmarshal([]byte(expectedJsonAsString), &expectedArray)
+ success = reflect.DeepEqual(actualArray, expectedArray)
+ if err != nil {
+ t.Fatalf("Unparsable expected JSON: %s, actual: %v, expected: %v", err, actualJsonAsString, expectedJsonAsString)
+ }
+ } else {
+ success = reflect.DeepEqual(actualMap, expectedMap)
+ }
+ if !success {
+ t.Log("---- expected -----")
+ t.Log(withLineNumbers(expectedJsonAsString))
+ t.Log("---- actual -----")
+ t.Log(withLineNumbers(actualJsonAsString))
+ t.Log("---- raw -----")
+ t.Log(actualJsonAsString)
+ t.Error("there are differences")
+ return false
+ }
+ return true
+}
+
+func indexOfNonMatchingLine(actual, expected string) int {
+ a := strings.Split(actual, "\n")
+ e := strings.Split(expected, "\n")
+ size := len(a)
+ if len(e) < len(a) {
+ size = len(e)
+ }
+ for i := 0; i < size; i++ {
+ if a[i] != e[i] {
+ return i
+ }
+ }
+ return -1
+}
+
+func withLineNumbers(content string) string {
+ var buffer bytes.Buffer
+ lines := strings.Split(content, "\n")
+ for i, each := range lines {
+ buffer.WriteString(fmt.Sprintf("%d:%s\n", i, each))
+ }
+ return buffer.String()
+}