Use vendored go-swagger (#8087)

* Use vendored go-swagger

* vendor go-swagger

* revert un wanteed change

* remove un-needed GO111MODULE

* Update Makefile

Co-Authored-By: techknowlogick <matti@mdranta.net>
This commit is contained in:
Antoine GIRARD
2019-09-04 21:53:54 +02:00
committed by Lauris BH
parent 4cb1bdddc8
commit 9fe4437bda
686 changed files with 143379 additions and 17 deletions

View File

@ -0,0 +1,5 @@
swagger
swagger.json
models
operations
cmd

View File

@ -0,0 +1,115 @@
package commands
import (
"encoding/json"
"errors"
"io/ioutil"
"log"
"os"
"github.com/go-openapi/loads"
"github.com/go-swagger/go-swagger/cmd/swagger/commands/diff"
)
// JSONFormat for json
const JSONFormat = "json"
// DiffCommand is a command that generates the diff of two swagger specs.
//
// There are no specific options for this expansion.
type DiffCommand struct {
OnlyBreakingChanges bool `long:"break" short:"b" description:"When present, only shows incompatible changes"`
Format string `long:"format" short:"f" description:"When present, writes output as json" default:"txt" choice:"txt" choice:"json"`
IgnoreFile string `long:"ignore" short:"i" description:"Exception file of diffs to ignore (copy output from json diff format)" default:"none specified"`
Destination string `long:"dest" short:"d" description:"Output destination file or stdout" default:"stdout"`
}
// Execute diffs the two specs provided
func (c *DiffCommand) Execute(args []string) error {
if len(args) != 2 {
msg := `missing arguments for diff command (use --help for more info)`
return errors.New(msg)
}
log.Println("Run Config:")
log.Printf("Spec1: %s", args[0])
log.Printf("Spec2: %s", args[1])
log.Printf("ReportOnlyBreakingChanges (-c) :%v", c.OnlyBreakingChanges)
log.Printf("OutputFormat (-f) :%s", c.Format)
log.Printf("IgnoreFile (-i) :%s", c.IgnoreFile)
log.Printf("Diff Report Destination (-d) :%s", c.Destination)
diffs, err := getDiffs(args[0], args[1])
if err != nil {
return err
}
ignores, err := readIgnores(c.IgnoreFile)
if err != nil {
return err
}
diffs = diffs.FilterIgnores(ignores)
if len(ignores) > 0 {
log.Printf("Diff Report Ignored Items from IgnoreFile")
for _, eachItem := range ignores {
log.Printf("%s", eachItem.String())
}
}
if c.Format == JSONFormat {
err = diffs.ReportAllDiffs(true)
if err != nil {
return err
}
} else {
if c.OnlyBreakingChanges {
err = diffs.ReportCompatibility()
} else {
err = diffs.ReportAllDiffs(false)
}
}
return err
}
func readIgnores(ignoreFile string) (diff.SpecDifferences, error) {
ignoreDiffs := diff.SpecDifferences{}
if ignoreFile == "none specified" {
return ignoreDiffs, nil
}
// Open our jsonFile
jsonFile, err := os.Open(ignoreFile)
// if we os.Open returns an error then handle it
if err != nil {
return nil, err
}
// defer the closing of our jsonFile so that we can parse it later on
defer jsonFile.Close()
byteValue, err := ioutil.ReadAll(jsonFile)
if err != nil {
return nil, err
}
// def
err = json.Unmarshal(byteValue, &ignoreDiffs)
if err != nil {
return nil, err
}
return ignoreDiffs, nil
}
func getDiffs(oldSpecPath, newSpecPath string) (diff.SpecDifferences, error) {
swaggerDoc1 := oldSpecPath
specDoc1, err := loads.Spec(swaggerDoc1)
if err != nil {
return nil, err
}
swaggerDoc2 := newSpecPath
specDoc2, err := loads.Spec(swaggerDoc2)
if err != nil {
return nil, err
}
return diff.Compare(specDoc1.Spec(), specDoc2.Spec())
}

View File

@ -0,0 +1,99 @@
package diff
// This is a simple DSL for diffing arrays
// FromArrayStruct utility struct to encompass diffing of string arrays
type FromArrayStruct struct {
from []string
}
// FromStringArray starts a fluent diff expression
func FromStringArray(from []string) FromArrayStruct {
return FromArrayStruct{from}
}
// DiffsTo completes a fluent dff expression
func (f FromArrayStruct) DiffsTo(toArray []string) (added, deleted, common []string) {
inFrom := 1
inTo := 2
m := make(map[string]int)
for _, item := range f.from {
m[item] = inFrom
}
for _, item := range toArray {
if _, ok := m[item]; ok {
m[item] |= inTo
} else {
m[item] = inTo
}
}
for key, val := range m {
switch val {
case inFrom:
deleted = append(deleted, key)
case inTo:
added = append(added, key)
default:
common = append(common, key)
}
}
return
}
// FromMapStruct utility struct to encompass diffing of string arrays
type FromMapStruct struct {
srcMap map[string]interface{}
}
// FromStringMap starts a comparison by declaring a source map
func FromStringMap(srcMap map[string]interface{}) FromMapStruct {
return FromMapStruct{srcMap}
}
// Pair stores a pair of items which share a key in two maps
type Pair struct {
First interface{}
Second interface{}
}
// DiffsTo - generates diffs for a comparison
func (f FromMapStruct) DiffsTo(destMap map[string]interface{}) (added, deleted, common map[string]interface{}) {
added = make(map[string]interface{})
deleted = make(map[string]interface{})
common = make(map[string]interface{})
inSrc := 1
inDest := 2
m := make(map[string]int)
// enter values for all items in the source array
for key := range f.srcMap {
m[key] = inSrc
}
// now either set or 'boolean or' a new flag if in the second collection
for key := range destMap {
if _, ok := m[key]; ok {
m[key] |= inDest
} else {
m[key] = inDest
}
}
// finally inspect the values and generate the left,right and shared collections
// for the shared items, store both values in case there's a diff
for key, val := range m {
switch val {
case inSrc:
deleted[key] = f.srcMap[key]
case inDest:
added[key] = destMap[key]
default:
common[key] = Pair{f.srcMap[key], destMap[key]}
}
}
return added, deleted, common
}

View File

@ -0,0 +1,90 @@
package diff
// CompatibilityPolicy decides which changes are breaking and which are not
type CompatibilityPolicy struct {
ForResponse map[SpecChangeCode]Compatibility
ForRequest map[SpecChangeCode]Compatibility
ForChange map[SpecChangeCode]Compatibility
}
var compatibility CompatibilityPolicy
func init() {
compatibility = CompatibilityPolicy{
ForResponse: map[SpecChangeCode]Compatibility{
AddedRequiredProperty: Breaking,
DeletedProperty: Breaking,
AddedProperty: NonBreaking,
DeletedResponse: Breaking,
AddedResponse: NonBreaking,
WidenedType: NonBreaking,
NarrowedType: NonBreaking,
ChangedType: Breaking,
ChangedToCompatibleType: NonBreaking,
AddedEnumValue: Breaking,
DeletedEnumValue: NonBreaking,
AddedResponseHeader: NonBreaking,
ChangedResponseHeader: Breaking,
DeletedResponseHeader: Breaking,
ChangedDescripton: NonBreaking,
AddedDescripton: NonBreaking,
DeletedDescripton: NonBreaking,
ChangedTag: NonBreaking,
AddedTag: NonBreaking,
DeletedTag: NonBreaking,
},
ForRequest: map[SpecChangeCode]Compatibility{
AddedRequiredProperty: Breaking,
DeletedProperty: Breaking,
AddedProperty: Breaking,
AddedOptionalParam: NonBreaking,
AddedRequiredParam: Breaking,
DeletedOptionalParam: NonBreaking,
DeletedRequiredParam: NonBreaking,
WidenedType: NonBreaking,
NarrowedType: Breaking,
ChangedType: Breaking,
ChangedToCompatibleType: NonBreaking,
ChangedOptionalToRequiredParam: Breaking,
ChangedRequiredToOptionalParam: NonBreaking,
AddedEnumValue: NonBreaking,
DeletedEnumValue: Breaking,
ChangedDescripton: NonBreaking,
AddedDescripton: NonBreaking,
DeletedDescripton: NonBreaking,
ChangedTag: NonBreaking,
AddedTag: NonBreaking,
DeletedTag: NonBreaking,
},
ForChange: map[SpecChangeCode]Compatibility{
NoChangeDetected: NonBreaking,
AddedEndpoint: NonBreaking,
DeletedEndpoint: Breaking,
DeletedDeprecatedEndpoint: NonBreaking,
AddedConsumesFormat: NonBreaking,
DeletedConsumesFormat: Breaking,
AddedProducesFormat: Breaking,
DeletedProducesFormat: NonBreaking,
AddedSchemes: NonBreaking,
DeletedSchemes: Breaking,
ChangedHostURL: Breaking,
ChangedBasePath: Breaking,
ChangedDescripton: NonBreaking,
AddedDescripton: NonBreaking,
DeletedDescripton: NonBreaking,
ChangedTag: NonBreaking,
AddedTag: NonBreaking,
DeletedTag: NonBreaking,
},
}
}
func getCompatibilityForChange(diffCode SpecChangeCode, where DataDirection) Compatibility {
if compat, commonChange := compatibility.ForChange[diffCode]; commonChange {
return compat
}
if where == Request {
return compatibility.ForRequest[diffCode]
}
return compatibility.ForResponse[diffCode]
}

View File

@ -0,0 +1,22 @@
package diff
// DifferenceLocation indicates where the difference occurs
type DifferenceLocation struct {
URL string `json:"url"`
Method string `json:"method,omitempty"`
Response int `json:"response,omitempty"`
Node *Node `json:"node,omitempty"`
}
// AddNode returns a copy of this location with the leaf node added
func (dl DifferenceLocation) AddNode(node *Node) DifferenceLocation {
newLoc := dl
if newLoc.Node != nil {
newLoc.Node = newLoc.Node.Copy()
newLoc.Node.AddLeafNode(node)
} else {
newLoc.Node = node
}
return newLoc
}

View File

@ -0,0 +1,276 @@
package diff
import (
"bytes"
"encoding/json"
"fmt"
)
// SpecChangeCode enumerates the various types of diffs from one spec to another
type SpecChangeCode int
const (
// NoChangeDetected - the specs have no changes
NoChangeDetected SpecChangeCode = iota
// DeletedProperty - A message property has been deleted in the new spec
DeletedProperty
// AddedProperty - A message property has been added in the new spec
AddedProperty
// AddedRequiredProperty - A required message property has been added in the new spec
AddedRequiredProperty
// DeletedOptionalParam - An endpoint parameter has been deleted in the new spec
DeletedOptionalParam
// ChangedDescripton - Changed a description
ChangedDescripton
// AddedDescripton - Added a description
AddedDescripton
// DeletedDescripton - Deleted a description
DeletedDescripton
// ChangedTag - Changed a tag
ChangedTag
// AddedTag - Added a tag
AddedTag
// DeletedTag - Deleted a tag
DeletedTag
// DeletedResponse - An endpoint response has been deleted in the new spec
DeletedResponse
// DeletedEndpoint - An endpoint has been deleted in the new spec
DeletedEndpoint
// DeletedDeprecatedEndpoint - A deprecated endpoint has been deleted in the new spec
DeletedDeprecatedEndpoint
// AddedRequiredParam - A required parameter has been added in the new spec
AddedRequiredParam
// DeletedRequiredParam - A required parameter has been deleted in the new spec
DeletedRequiredParam
// ChangedRequiredToOptional - A required parameter has been made optional in the new spec
ChangedRequiredToOptional
// AddedEndpoint - An endpoint has been added in the new spec
AddedEndpoint
// WidenedType - An type has been changed to a more permissive type eg int->string
WidenedType
// NarrowedType - An type has been changed to a less permissive type eg string->int
NarrowedType
// ChangedToCompatibleType - An type has been changed to a compatible type eg password->string
ChangedToCompatibleType
// ChangedType - An type has been changed to a type whose relative compatibility cannot be determined
ChangedType
// AddedEnumValue - An enum type has had a new potential value added to it
AddedEnumValue
// DeletedEnumValue - An enum type has had a existing value removed from it
DeletedEnumValue
// AddedOptionalParam - A new optional parameter has been added to the new spec
AddedOptionalParam
// ChangedOptionalToRequiredParam - An optional parameter is now required in the new spec
ChangedOptionalToRequiredParam
// ChangedRequiredToOptionalParam - An required parameter is now optional in the new spec
ChangedRequiredToOptionalParam
// AddedResponse An endpoint has new response code in the new spec
AddedResponse
// AddedConsumesFormat - a new consumes format (json/xml/yaml etc) has been added in the new spec
AddedConsumesFormat
// DeletedConsumesFormat - an existing format has been removed in the new spec
DeletedConsumesFormat
// AddedProducesFormat - a new produces format (json/xml/yaml etc) has been added in the new spec
AddedProducesFormat
// DeletedProducesFormat - an existing produces format has been removed in the new spec
DeletedProducesFormat
// AddedSchemes - a new scheme has been added to the new spec
AddedSchemes
// DeletedSchemes - a scheme has been removed from the new spec
DeletedSchemes
// ChangedHostURL - the host url has been changed. If this is used in the client generation, then clients will break.
ChangedHostURL
// ChangedBasePath - the host base path has been changed. If this is used in the client generation, then clients will break.
ChangedBasePath
// AddedResponseHeader Added a header Item
AddedResponseHeader
// ChangedResponseHeader Added a header Item
ChangedResponseHeader
// DeletedResponseHeader Added a header Item
DeletedResponseHeader
)
var toLongStringSpecChangeCode = map[SpecChangeCode]string{
NoChangeDetected: "No Change detected",
AddedEndpoint: "Added endpoint",
DeletedEndpoint: "Deleted endpoint",
DeletedDeprecatedEndpoint: "Deleted a deprecated endpoint",
AddedRequiredProperty: "Added required property",
DeletedProperty: "Deleted property",
ChangedDescripton: "Changed a description",
AddedDescripton: "Added a description",
DeletedDescripton: "Deleted a description",
ChangedTag: "Changed a tag",
AddedTag: "Added a tag",
DeletedTag: "Deleted a tag",
AddedProperty: "Added property",
AddedOptionalParam: "Added optional param",
AddedRequiredParam: "Added required param",
DeletedOptionalParam: "Deleted optional param",
DeletedRequiredParam: "Deleted required param",
DeletedResponse: "Deleted response",
AddedResponse: "Added response",
WidenedType: "Widened type",
NarrowedType: "Narrowed type",
ChangedType: "Changed type",
ChangedToCompatibleType: "Changed type to equivalent type",
ChangedOptionalToRequiredParam: "Changed optional param to required",
ChangedRequiredToOptionalParam: "Changed required param to optional",
AddedEnumValue: "Added possible enumeration(s)",
DeletedEnumValue: "Deleted possible enumeration(s)",
AddedConsumesFormat: "Added a consumes format",
DeletedConsumesFormat: "Deleted a consumes format",
AddedProducesFormat: "Added produces format",
DeletedProducesFormat: "Deleted produces format",
AddedSchemes: "Added schemes",
DeletedSchemes: "Deleted schemes",
ChangedHostURL: "Changed host URL",
ChangedBasePath: "Changed base path",
AddedResponseHeader: "Added response header",
ChangedResponseHeader: "Changed response header",
DeletedResponseHeader: "Deleted response header",
}
var toStringSpecChangeCode = map[SpecChangeCode]string{
AddedEndpoint: "AddedEndpoint",
NoChangeDetected: "NoChangeDetected",
DeletedEndpoint: "DeletedEndpoint",
DeletedDeprecatedEndpoint: "DeletedDeprecatedEndpoint",
AddedRequiredProperty: "AddedRequiredProperty",
DeletedProperty: "DeletedProperty",
AddedProperty: "AddedProperty",
ChangedDescripton: "ChangedDescription",
AddedDescripton: "AddedDescription",
DeletedDescripton: "DeletedDescription",
ChangedTag: "ChangedTag",
AddedTag: "AddedTag",
DeletedTag: "DeletedTag",
AddedOptionalParam: "AddedOptionalParam",
AddedRequiredParam: "AddedRequiredParam",
DeletedOptionalParam: "DeletedRequiredParam",
DeletedRequiredParam: "Deleted required param",
DeletedResponse: "DeletedResponse",
AddedResponse: "AddedResponse",
WidenedType: "WidenedType",
NarrowedType: "NarrowedType",
ChangedType: "ChangedType",
ChangedToCompatibleType: "ChangedToCompatibleType",
ChangedOptionalToRequiredParam: "ChangedOptionalToRequiredParam",
ChangedRequiredToOptionalParam: "ChangedRequiredToOptionalParam",
AddedEnumValue: "AddedEnumValue",
DeletedEnumValue: "DeletedEnumValue",
AddedConsumesFormat: "AddedConsumesFormat",
DeletedConsumesFormat: "DeletedConsumesFormat",
AddedProducesFormat: "AddedProducesFormat",
DeletedProducesFormat: "DeletedProducesFormat",
AddedSchemes: "AddedSchemes",
DeletedSchemes: "DeletedSchemes",
ChangedHostURL: "ChangedHostURL",
ChangedBasePath: "ChangedBasePath",
AddedResponseHeader: "AddedResponseHeader",
ChangedResponseHeader: "ChangedResponseHeader",
DeletedResponseHeader: "DeletedResponseHeader",
}
var toIDSpecChangeCode = map[string]SpecChangeCode{}
// Description returns an english version of this error
func (s *SpecChangeCode) Description() (result string) {
result, ok := toLongStringSpecChangeCode[*s]
if !ok {
fmt.Printf("WARNING: No description for %v", *s)
result = "UNDEFINED"
}
return
}
// MarshalJSON marshals the enum as a quoted json string
func (s *SpecChangeCode) MarshalJSON() ([]byte, error) {
return stringAsQuotedBytes(toStringSpecChangeCode[*s])
}
// UnmarshalJSON unmashalls a quoted json string to the enum value
func (s *SpecChangeCode) UnmarshalJSON(b []byte) error {
str, err := readStringFromByteStream(b)
if err != nil {
return err
}
// Note that if the string cannot be found then it will return an error to the caller.
val, ok := toIDSpecChangeCode[str]
if ok {
*s = val
} else {
return fmt.Errorf("unknown enum value. cannot unmarshal '%s'", str)
}
return nil
}
// Compatibility - whether this is a breaking or non-breaking change
type Compatibility int
const (
// Breaking this change could break existing clients
Breaking Compatibility = iota
// NonBreaking This is a backwards-compatible API change
NonBreaking
)
func (s Compatibility) String() string {
return toStringCompatibility[s]
}
var toStringCompatibility = map[Compatibility]string{
Breaking: "Breaking",
NonBreaking: "NonBreaking",
}
var toIDCompatibility = map[string]Compatibility{}
// MarshalJSON marshals the enum as a quoted json string
func (s *Compatibility) MarshalJSON() ([]byte, error) {
return stringAsQuotedBytes(toStringCompatibility[*s])
}
// UnmarshalJSON unmashals a quoted json string to the enum value
func (s *Compatibility) UnmarshalJSON(b []byte) error {
str, err := readStringFromByteStream(b)
if err != nil {
return err
}
// Note that if the string cannot be found then it will return an error to the caller.
val, ok := toIDCompatibility[str]
if ok {
*s = val
} else {
return fmt.Errorf("unknown enum value. cannot unmarshal '%s'", str)
}
return nil
}
func stringAsQuotedBytes(str string) ([]byte, error) {
buffer := bytes.NewBufferString(`"`)
buffer.WriteString(str)
buffer.WriteString(`"`)
return buffer.Bytes(), nil
}
func readStringFromByteStream(b []byte) (string, error) {
var j string
err := json.Unmarshal(b, &j)
if err != nil {
return "", err
}
return j, nil
}
func init() {
for key, val := range toStringSpecChangeCode {
toIDSpecChangeCode[val] = key
}
for key, val := range toStringCompatibility {
toIDCompatibility[val] = key
}
}

View File

@ -0,0 +1,47 @@
package diff
// Node is the position od a diff in a spec
type Node struct {
Field string `json:"name,omitempty"`
TypeName string `json:"type,omitempty"`
IsArray bool `json:"is_array,omitempty"`
ChildNode *Node `json:"child,omitempty"`
}
// String std string render
func (n *Node) String() string {
name := n.Field
if n.IsArray {
name = "array[" + n.TypeName + "]"
}
if n.ChildNode != nil {
return name + "." + n.ChildNode.String()
}
if len(n.TypeName) > 0 {
return name + " : " + n.TypeName
}
return name
}
// AddLeafNode Adds (recursive) a Child to the first non-nil child found
func (n *Node) AddLeafNode(toAdd *Node) *Node {
if n.ChildNode == nil {
n.ChildNode = toAdd
} else {
n.ChildNode.AddLeafNode(toAdd)
}
return n
}
//Copy deep copy of this node and children
func (n Node) Copy() *Node {
newNode := n
if newNode.ChildNode != nil {
n.ChildNode = newNode.ChildNode.Copy()
}
return &newNode
}

View File

@ -0,0 +1,169 @@
package diff
import (
"bytes"
"encoding/json"
"fmt"
"net/url"
"strings"
"github.com/go-openapi/spec"
)
// ArrayType const for array
var ArrayType = "array"
// Compare returns the result of analysing breaking and non breaking changes
// between to Swagger specs
func Compare(spec1, spec2 *spec.Swagger) (diffs SpecDifferences, err error) {
analyser := NewSpecAnalyser()
err = analyser.Analyse(spec1, spec2)
if err != nil {
return nil, err
}
diffs = analyser.Diffs
return
}
// PathItemOp - combines path and operation into a single keyed entity
type PathItemOp struct {
ParentPathItem *spec.PathItem `json:"pathitem"`
Operation *spec.Operation `json:"operation"`
}
// URLMethod - combines url and method into a single keyed entity
type URLMethod struct {
Path string `json:"path"`
Method string `json:"method"`
}
// DataDirection indicates the direction of change Request vs Response
type DataDirection int
const (
// Request Used for messages/param diffs in a request
Request DataDirection = iota
// Response Used for messages/param diffs in a response
Response
)
func getParams(pathParams, opParams []spec.Parameter, location string) map[string]spec.Parameter {
params := map[string]spec.Parameter{}
// add shared path params
for _, eachParam := range pathParams {
if eachParam.In == location {
params[eachParam.Name] = eachParam
}
}
// add any overridden params
for _, eachParam := range opParams {
if eachParam.In == location {
params[eachParam.Name] = eachParam
}
}
return params
}
func getNameOnlyDiffNode(forLocation string) *Node {
node := Node{
Field: forLocation,
}
return &node
}
func getSimpleSchemaDiffNode(name string, schema *spec.SimpleSchema) *Node {
node := Node{
Field: name,
}
if schema != nil {
node.TypeName, node.IsArray = getSimpleSchemaType(schema)
}
return &node
}
func getSchemaDiffNode(name string, schema *spec.Schema) *Node {
node := Node{
Field: name,
}
if schema != nil {
node.TypeName, node.IsArray = getSchemaType(&schema.SchemaProps)
}
return &node
}
func definitonFromURL(url *url.URL) string {
if url == nil {
return ""
}
fragmentParts := strings.Split(url.Fragment, "/")
numParts := len(fragmentParts)
if numParts == 0 {
return ""
}
return fragmentParts[numParts-1]
}
func getSimpleSchemaType(schema *spec.SimpleSchema) (typeName string, isArray bool) {
typeName = schema.Type
if typeName == ArrayType {
typeName, _ = getSimpleSchemaType(&schema.Items.SimpleSchema)
return typeName, true
}
return typeName, false
}
func getSchemaType(schema *spec.SchemaProps) (typeName string, isArray bool) {
refStr := definitonFromURL(schema.Ref.GetURL())
if len(refStr) > 0 {
return refStr, false
}
typeName = schema.Type[0]
if typeName == ArrayType {
typeName, _ = getSchemaType(&schema.Items.Schema.SchemaProps)
return typeName, true
}
return typeName, false
}
func primitiveTypeString(typeName, typeFormat string) string {
if typeFormat != "" {
return fmt.Sprintf("%s.%s", typeName, typeFormat)
}
return typeName
}
// TypeDiff - describes a primitive type change
type TypeDiff struct {
Change SpecChangeCode `json:"change-type,omitempty"`
Description string `json:"description,omitempty"`
FromType string `json:"from-type,omitempty"`
ToType string `json:"to-type,omitempty"`
}
// didn't use 'width' so as not to confuse with bit width
var numberWideness = map[string]int{
"number": 3,
"number.double": 3,
"double": 3,
"number.float": 2,
"float": 2,
"long": 1,
"integer.int64": 1,
"integer": 0,
"integer.int32": 0,
}
func prettyprint(b []byte) ([]byte, error) {
var out bytes.Buffer
err := json.Indent(&out, b, "", " ")
return out.Bytes(), err
}
// JSONMarshal allows the item to be correctly rendered to json
func JSONMarshal(t interface{}) ([]byte, error) {
buffer := &bytes.Buffer{}
encoder := json.NewEncoder(buffer)
encoder.SetEscapeHTML(false)
err := encoder.Encode(t)
return buffer.Bytes(), err
}

View File

@ -0,0 +1,654 @@
package diff
import (
"fmt"
"strings"
"github.com/go-openapi/spec"
)
const StringType = "string"
// URLMethodResponse encapsulates these three elements to act as a map key
type URLMethodResponse struct {
Path string `json:"path"`
Method string `json:"method"`
Response string `json:"response"`
}
// MarshalText - for serializing as a map key
func (p URLMethod) MarshalText() (text []byte, err error) {
return []byte(fmt.Sprintf("%s %s", p.Path, p.Method)), nil
}
// URLMethods allows iteration of endpoints based on url and method
type URLMethods map[URLMethod]*PathItemOp
// SpecAnalyser contains all the differences for a Spec
type SpecAnalyser struct {
Diffs SpecDifferences
urlMethods1 URLMethods
urlMethods2 URLMethods
Definitions1 spec.Definitions
Definitions2 spec.Definitions
AlreadyComparedDefinitions map[string]bool
}
// NewSpecAnalyser returns an empty SpecDiffs
func NewSpecAnalyser() *SpecAnalyser {
return &SpecAnalyser{
Diffs: SpecDifferences{},
}
}
// Analyse the differences in two specs
func (sd *SpecAnalyser) Analyse(spec1, spec2 *spec.Swagger) error {
sd.Definitions1 = spec1.Definitions
sd.Definitions2 = spec2.Definitions
sd.urlMethods1 = getURLMethodsFor(spec1)
sd.urlMethods2 = getURLMethodsFor(spec2)
sd.analyseSpecMetadata(spec1, spec2)
sd.analyseEndpoints()
sd.analyseParams()
sd.analyseEndpointData()
sd.analyseResponseParams()
return nil
}
func (sd *SpecAnalyser) analyseSpecMetadata(spec1, spec2 *spec.Swagger) {
// breaking if it no longer consumes any formats
added, deleted, _ := FromStringArray(spec1.Consumes).DiffsTo(spec2.Consumes)
node := getNameOnlyDiffNode("Spec")
location := DifferenceLocation{Node: node}
consumesLoation := location.AddNode(getNameOnlyDiffNode("consumes"))
for _, eachAdded := range added {
sd.Diffs = sd.Diffs.addDiff(
SpecDifference{DifferenceLocation: consumesLoation, Code: AddedConsumesFormat, Compatibility: NonBreaking, DiffInfo: eachAdded})
}
for _, eachDeleted := range deleted {
sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: consumesLoation, Code: DeletedConsumesFormat, Compatibility: Breaking, DiffInfo: eachDeleted})
}
// // breaking if it no longer produces any formats
added, deleted, _ = FromStringArray(spec1.Produces).DiffsTo(spec2.Produces)
producesLocation := location.AddNode(getNameOnlyDiffNode("produces"))
for _, eachAdded := range added {
sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: producesLocation, Code: AddedProducesFormat, Compatibility: NonBreaking, DiffInfo: eachAdded})
}
for _, eachDeleted := range deleted {
sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: producesLocation, Code: DeletedProducesFormat, Compatibility: Breaking, DiffInfo: eachDeleted})
}
// // breaking if it no longer supports a scheme
added, deleted, _ = FromStringArray(spec1.Schemes).DiffsTo(spec2.Schemes)
schemesLocation := location.AddNode(getNameOnlyDiffNode("schemes"))
for _, eachAdded := range added {
sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: schemesLocation, Code: AddedSchemes, Compatibility: NonBreaking, DiffInfo: eachAdded})
}
for _, eachDeleted := range deleted {
sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: schemesLocation, Code: DeletedSchemes, Compatibility: Breaking, DiffInfo: eachDeleted})
}
// // host should be able to change without any issues?
sd.analyseMetaDataProperty(spec1.Info.Description, spec2.Info.Description, ChangedDescripton, NonBreaking)
// // host should be able to change without any issues?
sd.analyseMetaDataProperty(spec1.Host, spec2.Host, ChangedHostURL, Breaking)
// sd.Host = compareStrings(spec1.Host, spec2.Host)
// // Base Path change will break non generated clients
sd.analyseMetaDataProperty(spec1.BasePath, spec2.BasePath, ChangedBasePath, Breaking)
// TODO: what to do about security?
// Missing security scheme will break a client
// Security []map[string][]string `json:"security,omitempty"`
// Tags []Tag `json:"tags,omitempty"`
// ExternalDocs *ExternalDocumentation `json:"externalDocs,omitempty"`
}
func (sd *SpecAnalyser) analyseEndpoints() {
sd.findDeletedEndpoints()
sd.findAddedEndpoints()
}
func (sd *SpecAnalyser) analyseEndpointData() {
for URLMethod, op2 := range sd.urlMethods2 {
if op1, ok := sd.urlMethods1[URLMethod]; ok {
addedTags, deletedTags, _ := FromStringArray(op1.Operation.Tags).DiffsTo(op2.Operation.Tags)
location := DifferenceLocation{URL: URLMethod.Path, Method: URLMethod.Method}
for _, eachAddedTag := range addedTags {
sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: location, Code: AddedTag, DiffInfo: eachAddedTag})
}
for _, eachDeletedTag := range deletedTags {
sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: location, Code: DeletedTag, DiffInfo: eachDeletedTag})
}
sd.compareDescripton(location, op1.Operation.Description, op2.Operation.Description)
}
}
}
func (sd *SpecAnalyser) analyseParams() {
locations := []string{"query", "path", "body", "header"}
for _, paramLocation := range locations {
rootNode := getNameOnlyDiffNode(strings.Title(paramLocation))
for URLMethod, op2 := range sd.urlMethods2 {
if op1, ok := sd.urlMethods1[URLMethod]; ok {
params1 := getParams(op1.ParentPathItem.Parameters, op1.Operation.Parameters, paramLocation)
params2 := getParams(op2.ParentPathItem.Parameters, op2.Operation.Parameters, paramLocation)
location := DifferenceLocation{URL: URLMethod.Path, Method: URLMethod.Method, Node: rootNode}
// detect deleted params
for paramName1, param1 := range params1 {
if _, ok := params2[paramName1]; !ok {
childLocation := location.AddNode(getSchemaDiffNode(paramName1, param1.Schema))
code := DeletedOptionalParam
if param1.Required {
code = DeletedRequiredParam
}
sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: childLocation, Code: code})
}
}
// detect added changed params
for paramName2, param2 := range params2 {
//changed?
if param1, ok := params1[paramName2]; ok {
sd.compareParams(URLMethod, paramLocation, paramName2, param1, param2)
} else {
// Added
childLocation := location.AddNode(getSchemaDiffNode(paramName2, param2.Schema))
code := AddedOptionalParam
if param2.Required {
code = AddedRequiredParam
}
sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: childLocation, Code: code})
}
}
}
}
}
}
func (sd *SpecAnalyser) analyseResponseParams() {
// Loop through url+methods in spec 2 - check deleted and changed
for URLMethod2, op2 := range sd.urlMethods2 {
if op1, ok := sd.urlMethods1[URLMethod2]; ok {
// compare responses for url and method
op1Responses := op1.Operation.Responses.StatusCodeResponses
op2Responses := op2.Operation.Responses.StatusCodeResponses
// deleted responses
for code1 := range op1Responses {
if _, ok := op2Responses[code1]; !ok {
location := DifferenceLocation{URL: URLMethod2.Path, Method: URLMethod2.Method, Response: code1}
sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: location, Code: DeletedResponse})
}
}
// Added updated Response Codes
for code2, op2Response := range op2Responses {
if op1Response, ok := op1Responses[code2]; ok {
op1Headers := op1Response.ResponseProps.Headers
headerRootNode := getNameOnlyDiffNode("Headers")
location := DifferenceLocation{URL: URLMethod2.Path, Method: URLMethod2.Method, Response: code2, Node: headerRootNode}
// Iterate Spec2 Headers looking for added and updated
for op2HeaderName, op2Header := range op2Response.ResponseProps.Headers {
if op1Header, ok := op1Headers[op2HeaderName]; ok {
sd.compareSimpleSchema(location.AddNode(getNameOnlyDiffNode(op2HeaderName)),
&op1Header.SimpleSchema,
&op2Header.SimpleSchema, false, false)
} else {
sd.Diffs = sd.Diffs.addDiff(SpecDifference{
DifferenceLocation: location.AddNode(getNameOnlyDiffNode(op2HeaderName)),
Code: AddedResponseHeader})
}
}
for op1HeaderName := range op1Response.ResponseProps.Headers {
if _, ok := op2Response.ResponseProps.Headers[op1HeaderName]; !ok {
sd.Diffs = sd.Diffs.addDiff(SpecDifference{
DifferenceLocation: location.AddNode(getNameOnlyDiffNode(op1HeaderName)),
Code: DeletedResponseHeader})
}
}
responseLocation := DifferenceLocation{URL: URLMethod2.Path, Method: URLMethod2.Method, Response: code2}
sd.compareDescripton(responseLocation, op1Response.Description, op2Response.Description)
if op1Response.Schema != nil {
sd.compareSchema(
DifferenceLocation{URL: URLMethod2.Path, Method: URLMethod2.Method, Response: code2},
op1Response.Schema,
op2Response.Schema, true, true)
}
} else {
sd.Diffs = sd.Diffs.addDiff(SpecDifference{
DifferenceLocation: DifferenceLocation{URL: URLMethod2.Path, Method: URLMethod2.Method, Response: code2},
Code: AddedResponse})
}
}
}
}
}
func addTypeDiff(diffs []TypeDiff, diff TypeDiff) []TypeDiff {
if diff.Change != NoChangeDetected {
diffs = append(diffs, diff)
}
return diffs
}
// CheckToFromPrimitiveType check for diff to or from a primitive
func (sd *SpecAnalyser) CheckToFromPrimitiveType(diffs []TypeDiff, type1, type2 spec.SchemaProps) []TypeDiff {
type1IsPrimitive := len(type1.Type) > 0
type2IsPrimitive := len(type2.Type) > 0
// Primitive to Obj or Obj to Primitive
if type1IsPrimitive && !type2IsPrimitive {
return addTypeDiff(diffs, TypeDiff{Change: ChangedType, FromType: type1.Type[0], ToType: "obj"})
}
if !type1IsPrimitive && type2IsPrimitive {
return addTypeDiff(diffs, TypeDiff{Change: ChangedType, FromType: type2.Type[0], ToType: "obj"})
}
return diffs
}
// CheckToFromArrayType check for changes to or from an Array type
func (sd *SpecAnalyser) CheckToFromArrayType(diffs []TypeDiff, type1, type2 spec.SchemaProps) []TypeDiff {
// Single to Array or Array to Single
type1Array := type1.Type[0] == ArrayType
type2Array := type2.Type[0] == ArrayType
if type1Array && !type2Array {
return addTypeDiff(diffs, TypeDiff{Change: ChangedType, FromType: "obj", ToType: type2.Type[0]})
}
if !type1Array && type2Array {
return addTypeDiff(diffs, TypeDiff{Change: ChangedType, FromType: type1.Type[0], ToType: ArrayType})
}
if type1Array && type2Array {
// array
// TODO: Items??
diffs = addTypeDiff(diffs, compareIntValues("MaxItems", type1.MaxItems, type2.MaxItems, WidenedType, NarrowedType))
diffs = addTypeDiff(diffs, compareIntValues("MinItems", type1.MinItems, type2.MinItems, NarrowedType, WidenedType))
}
return diffs
}
// CheckStringTypeChanges checks for changes to or from a string type
func (sd *SpecAnalyser) CheckStringTypeChanges(diffs []TypeDiff, type1, type2 spec.SchemaProps) []TypeDiff {
// string changes
if type1.Type[0] == StringType &&
type2.Type[0] == StringType {
diffs = addTypeDiff(diffs, compareIntValues("MinLength", type1.MinLength, type2.MinLength, NarrowedType, WidenedType))
diffs = addTypeDiff(diffs, compareIntValues("MaxLength", type1.MinLength, type2.MinLength, WidenedType, NarrowedType))
if type1.Pattern != type2.Pattern {
diffs = addTypeDiff(diffs, TypeDiff{Change: ChangedType, Description: fmt.Sprintf("Pattern Changed:%s->%s", type1.Pattern, type2.Pattern)})
}
if type1.Type[0] == StringType {
if len(type1.Enum) > 0 {
enumDiffs := sd.compareEnums(type1.Enum, type2.Enum)
diffs = append(diffs, enumDiffs...)
}
}
}
return diffs
}
// CheckNumericTypeChanges checks for changes to or from a numeric type
func (sd *SpecAnalyser) CheckNumericTypeChanges(diffs []TypeDiff, type1, type2 spec.SchemaProps) []TypeDiff {
// Number
_, type1IsNumeric := numberWideness[type1.Type[0]]
_, type2IsNumeric := numberWideness[type2.Type[0]]
if type1IsNumeric && type2IsNumeric {
diffs = addTypeDiff(diffs, compareFloatValues("Maximum", type1.Maximum, type2.Maximum, WidenedType, NarrowedType))
diffs = addTypeDiff(diffs, compareFloatValues("Minimum", type1.Minimum, type2.Minimum, NarrowedType, WidenedType))
if type1.ExclusiveMaximum && !type2.ExclusiveMaximum {
diffs = addTypeDiff(diffs, TypeDiff{Change: WidenedType, Description: fmt.Sprintf("Exclusive Maximum Removed:%v->%v", type1.ExclusiveMaximum, type2.ExclusiveMaximum)})
}
if !type1.ExclusiveMaximum && type2.ExclusiveMaximum {
diffs = addTypeDiff(diffs, TypeDiff{Change: NarrowedType, Description: fmt.Sprintf("Exclusive Maximum Added:%v->%v", type1.ExclusiveMaximum, type2.ExclusiveMaximum)})
}
if type1.ExclusiveMinimum && !type2.ExclusiveMinimum {
diffs = addTypeDiff(diffs, TypeDiff{Change: WidenedType, Description: fmt.Sprintf("Exclusive Minimum Removed:%v->%v", type1.ExclusiveMaximum, type2.ExclusiveMaximum)})
}
if !type1.ExclusiveMinimum && type2.ExclusiveMinimum {
diffs = addTypeDiff(diffs, TypeDiff{Change: NarrowedType, Description: fmt.Sprintf("Exclusive Minimum Added:%v->%v", type1.ExclusiveMinimum, type2.ExclusiveMinimum)})
}
}
return diffs
}
// CompareTypes computes type specific property diffs
func (sd *SpecAnalyser) CompareTypes(type1, type2 spec.SchemaProps) []TypeDiff {
diffs := []TypeDiff{}
diffs = sd.CheckToFromPrimitiveType(diffs, type1, type2)
if len(diffs) > 0 {
return diffs
}
diffs = sd.CheckToFromArrayType(diffs, type1, type2)
if len(diffs) > 0 {
return diffs
}
// check type hierarchy change eg string -> integer = NarrowedChange
//Type
//Format
if type1.Type[0] != type2.Type[0] ||
type1.Format != type2.Format {
diff := getTypeHierarchyChange(primitiveTypeString(type1.Type[0], type1.Format), primitiveTypeString(type2.Type[0], type2.Format))
diffs = addTypeDiff(diffs, diff)
}
diffs = sd.CheckStringTypeChanges(diffs, type1, type2)
if len(diffs) > 0 {
return diffs
}
diffs = sd.CheckNumericTypeChanges(diffs, type1, type2)
if len(diffs) > 0 {
return diffs
}
return diffs
}
func (sd *SpecAnalyser) compareParams(urlMethod URLMethod, location string, name string, param1, param2 spec.Parameter) {
diffLocation := DifferenceLocation{URL: urlMethod.Path, Method: urlMethod.Method}
childLocation := diffLocation.AddNode(getNameOnlyDiffNode(strings.Title(location)))
paramLocation := diffLocation.AddNode(getNameOnlyDiffNode(name))
sd.compareDescripton(paramLocation, param1.Description, param2.Description)
if param1.Schema != nil && param2.Schema != nil {
childLocation = childLocation.AddNode(getSchemaDiffNode(name, param2.Schema))
sd.compareSchema(childLocation, param1.Schema, param2.Schema, param1.Required, param2.Required)
}
diffs := sd.CompareTypes(forParam(param1), forParam(param2))
childLocation = childLocation.AddNode(getSchemaDiffNode(name, param2.Schema))
for _, eachDiff := range diffs {
sd.Diffs = sd.Diffs.addDiff(SpecDifference{
DifferenceLocation: childLocation,
Code: eachDiff.Change,
DiffInfo: eachDiff.Description})
}
if param1.Required != param2.Required {
code := ChangedRequiredToOptionalParam
if param2.Required {
code = ChangedOptionalToRequiredParam
}
sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: childLocation, Code: code})
}
}
func (sd *SpecAnalyser) compareSimpleSchema(location DifferenceLocation, schema1, schema2 *spec.SimpleSchema, required1, required2 bool) {
if schema1 == nil || schema2 == nil {
return
}
if schema1.Type == ArrayType {
refSchema1 := schema1.Items.SimpleSchema
refSchema2 := schema2.Items.SimpleSchema
childLocation := location.AddNode(getSimpleSchemaDiffNode("", schema1))
sd.compareSimpleSchema(childLocation, &refSchema1, &refSchema2, required1, required2)
return
}
if required1 != required2 {
code := AddedRequiredProperty
if required1 {
code = ChangedRequiredToOptional
}
sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: location, Code: code})
}
}
func (sd *SpecAnalyser) compareDescripton(location DifferenceLocation, desc1, desc2 string) {
if desc1 != desc2 {
code := ChangedDescripton
if len(desc1) > 0 {
code = DeletedDescripton
} else if len(desc2) > 0 {
code = AddedDescripton
}
sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: location, Code: code})
}
}
func (sd *SpecAnalyser) compareSchema(location DifferenceLocation, schema1, schema2 *spec.Schema, required1, required2 bool) {
if schema1 == nil || schema2 == nil {
return
}
sd.compareDescripton(location, schema1.Description, schema2.Description)
if len(schema1.Type) == 0 {
refSchema1, definition1 := sd.schemaFromRef(schema1, &sd.Definitions1)
refSchema2, definition2 := sd.schemaFromRef(schema2, &sd.Definitions2)
if len(definition1) > 0 {
info := fmt.Sprintf("[%s -> %s]", definition1, definition2)
if definition1 != definition2 {
sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: location,
Code: ChangedType,
DiffInfo: info,
})
}
sd.compareSchema(location, refSchema1, refSchema2, required1, required2)
return
}
} else {
if schema1.Type[0] == ArrayType {
refSchema1, definition1 := sd.schemaFromRef(schema1.Items.Schema, &sd.Definitions1)
refSchema2, _ := sd.schemaFromRef(schema2.Items.Schema, &sd.Definitions2)
if len(definition1) > 0 {
childLocation := location.AddNode(getSchemaDiffNode("", schema1))
sd.compareSchema(childLocation, refSchema1, refSchema2, required1, required2)
return
}
}
diffs := sd.CompareTypes(schema1.SchemaProps, schema2.SchemaProps)
for _, eachTypeDiff := range diffs {
if eachTypeDiff.Change != NoChangeDetected {
sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: location, Code: eachTypeDiff.Change, DiffInfo: eachTypeDiff.Description})
}
}
}
if required1 != required2 {
code := AddedRequiredProperty
if required1 {
code = ChangedRequiredToOptional
}
sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: location, Code: code})
}
requiredProps2 := sliceToStrMap(schema2.Required)
requiredProps1 := sliceToStrMap(schema1.Required)
schema1Props := sd.propertiesFor(schema1, &sd.Definitions1)
schema2Props := sd.propertiesFor(schema2, &sd.Definitions2)
// find deleted and changed properties
for eachProp1Name, eachProp1 := range schema1Props {
eachProp1 := eachProp1
_, required1 := requiredProps1[eachProp1Name]
_, required2 := requiredProps2[eachProp1Name]
childLoc := sd.addChildDiffNode(location, eachProp1Name, &eachProp1)
if eachProp2, ok := schema2Props[eachProp1Name]; ok {
sd.compareSchema(childLoc, &eachProp1, &eachProp2, required1, required2)
sd.compareDescripton(childLoc, eachProp1.Description, eachProp2.Description)
} else {
sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: childLoc, Code: DeletedProperty})
}
}
// find added properties
for eachProp2Name, eachProp2 := range schema2.Properties {
eachProp2 := eachProp2
if _, ok := schema1.Properties[eachProp2Name]; !ok {
childLoc := sd.addChildDiffNode(location, eachProp2Name, &eachProp2)
_, required2 := requiredProps2[eachProp2Name]
code := AddedProperty
if required2 {
code = AddedRequiredProperty
}
sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: childLoc, Code: code})
}
}
}
func (sd *SpecAnalyser) addChildDiffNode(location DifferenceLocation, propName string, propSchema *spec.Schema) DifferenceLocation {
newLoc := location
if newLoc.Node != nil {
newLoc.Node = newLoc.Node.Copy()
}
childNode := sd.fromSchemaProps(propName, &propSchema.SchemaProps)
if newLoc.Node != nil {
newLoc.Node.AddLeafNode(&childNode)
} else {
newLoc.Node = &childNode
}
return newLoc
}
func (sd *SpecAnalyser) fromSchemaProps(fieldName string, props *spec.SchemaProps) Node {
node := Node{}
node.IsArray = props.Type[0] == ArrayType
if !node.IsArray {
node.TypeName = props.Type[0]
}
node.Field = fieldName
return node
}
func (sd *SpecAnalyser) compareEnums(left, right []interface{}) []TypeDiff {
diffs := []TypeDiff{}
leftStrs := []string{}
rightStrs := []string{}
for _, eachLeft := range left {
leftStrs = append(leftStrs, fmt.Sprintf("%v", eachLeft))
}
for _, eachRight := range right {
rightStrs = append(rightStrs, fmt.Sprintf("%v", eachRight))
}
added, deleted, _ := FromStringArray(leftStrs).DiffsTo(rightStrs)
if len(added) > 0 {
typeChange := strings.Join(added, ",")
diffs = append(diffs, TypeDiff{Change: AddedEnumValue, Description: typeChange})
}
if len(deleted) > 0 {
typeChange := strings.Join(deleted, ",")
diffs = append(diffs, TypeDiff{Change: DeletedEnumValue, Description: typeChange})
}
return diffs
}
func (sd *SpecAnalyser) findAddedEndpoints() {
for URLMethod := range sd.urlMethods2 {
if _, ok := sd.urlMethods1[URLMethod]; !ok {
sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: DifferenceLocation{URL: URLMethod.Path, Method: URLMethod.Method}, Code: AddedEndpoint})
}
}
}
func (sd *SpecAnalyser) findDeletedEndpoints() {
for eachURLMethod, operation1 := range sd.urlMethods1 {
code := DeletedEndpoint
if (operation1.ParentPathItem.Options != nil && operation1.ParentPathItem.Options.Deprecated) ||
(operation1.Operation.Deprecated) {
code = DeletedDeprecatedEndpoint
}
if _, ok := sd.urlMethods2[eachURLMethod]; !ok {
sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: DifferenceLocation{URL: eachURLMethod.Path, Method: eachURLMethod.Method}, Code: code})
}
}
}
func (sd *SpecAnalyser) analyseMetaDataProperty(item1, item2 string, codeIfDiff SpecChangeCode, compatIfDiff Compatibility) {
if item1 != item2 {
diffSpec := fmt.Sprintf("%s -> %s", item1, item2)
sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: DifferenceLocation{Node: &Node{Field: "Spec Metadata"}}, Code: codeIfDiff, Compatibility: compatIfDiff, DiffInfo: diffSpec})
}
}
func (sd *SpecAnalyser) schemaFromRef(schema *spec.Schema, defns *spec.Definitions) (actualSchema *spec.Schema, definitionName string) {
ref := schema.Ref
url := ref.GetURL()
if url == nil {
return schema, ""
}
fragmentParts := strings.Split(url.Fragment, "/")
numParts := len(fragmentParts)
if numParts == 0 {
return schema, ""
}
definitionName = fragmentParts[numParts-1]
foundSchema, ok := (*defns)[definitionName]
if !ok {
return nil, definitionName
}
actualSchema = &foundSchema
return
}
func (sd *SpecAnalyser) propertiesFor(schema *spec.Schema, defns *spec.Definitions) map[string]spec.Schema {
schemaFromRef, _ := sd.schemaFromRef(schema, defns)
schema = schemaFromRef
props := map[string]spec.Schema{}
if schema.Properties != nil {
for name, prop := range schema.Properties {
prop := prop
eachProp, _ := sd.schemaFromRef(&prop, defns)
props[name] = *eachProp
}
}
for _, eachAllOf := range schema.AllOf {
eachAllOf := eachAllOf
eachAllOfActual, _ := sd.schemaFromRef(&eachAllOf, defns)
for name, prop := range eachAllOfActual.Properties {
prop := prop
eachProp, _ := sd.schemaFromRef(&prop, defns)
props[name] = *eachProp
}
}
return props
}

View File

@ -0,0 +1,190 @@
package diff
import (
"fmt"
"log"
"sort"
)
// SpecDifference encapsulates the details of an individual diff in part of a spec
type SpecDifference struct {
DifferenceLocation DifferenceLocation `json:"location"`
Code SpecChangeCode `json:"code"`
Compatibility Compatibility `json:"compatibility"`
DiffInfo string `json:"info,omitempty"`
}
// SpecDifferences list of differences
type SpecDifferences []SpecDifference
// Matches returns true if the diff matches another
func (sd SpecDifference) Matches(other SpecDifference) bool {
return sd.Code == other.Code &&
sd.Compatibility == other.Compatibility &&
sd.DiffInfo == other.DiffInfo &&
equalLocations(sd.DifferenceLocation, other.DifferenceLocation)
}
func equalLocations(a, b DifferenceLocation) bool {
return a.Method == b.Method &&
a.Response == b.Response &&
a.URL == b.URL &&
equalNodes(a.Node, b.Node)
}
func equalNodes(a, b *Node) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
return a.Field == b.Field &&
a.IsArray == b.IsArray &&
a.TypeName == b.TypeName &&
equalNodes(a.ChildNode, b.ChildNode)
}
// BreakingChangeCount Calculates the breaking change count
func (sd SpecDifferences) BreakingChangeCount() int {
count := 0
for _, eachDiff := range sd {
if eachDiff.Compatibility == Breaking {
count++
}
}
return count
}
// FilterIgnores returns a copy of the list without the items in the specified ignore list
func (sd SpecDifferences) FilterIgnores(ignores SpecDifferences) SpecDifferences {
newDiffs := SpecDifferences{}
for _, eachDiff := range sd {
if !ignores.Contains(eachDiff) {
newDiffs = newDiffs.addDiff(eachDiff)
}
}
return newDiffs
}
// Contains Returns true if the item contains the specified item
func (sd SpecDifferences) Contains(diff SpecDifference) bool {
for _, eachDiff := range sd {
if eachDiff.Matches(diff) {
return true
}
}
return false
}
// String std string renderer
func (sd SpecDifference) String() string {
isResponse := sd.DifferenceLocation.Response > 0
hasMethod := len(sd.DifferenceLocation.Method) > 0
hasURL := len(sd.DifferenceLocation.URL) > 0
prefix := ""
direction := ""
if isResponse {
direction = " Response"
if hasURL {
if hasMethod {
prefix = fmt.Sprintf("%s:%s -> %d", sd.DifferenceLocation.URL, sd.DifferenceLocation.Method, sd.DifferenceLocation.Response)
} else {
prefix = fmt.Sprintf("%s ", sd.DifferenceLocation.URL)
}
}
} else {
if hasURL {
if hasMethod {
direction = " Request"
prefix = fmt.Sprintf("%s:%s", sd.DifferenceLocation.URL, sd.DifferenceLocation.Method)
} else {
prefix = fmt.Sprintf("%s ", sd.DifferenceLocation.URL)
}
} else {
prefix = " Metadata"
}
}
paramOrPropertyLocation := ""
if sd.DifferenceLocation.Node != nil {
paramOrPropertyLocation = " - " + sd.DifferenceLocation.Node.String() + " "
}
optionalInfo := ""
if sd.DiffInfo != "" {
optionalInfo = fmt.Sprintf(" <%s>", sd.DiffInfo)
}
return fmt.Sprintf("%s%s%s- %s%s", prefix, direction, paramOrPropertyLocation, sd.Code.Description(), optionalInfo)
}
func (sd SpecDifferences) addDiff(diff SpecDifference) SpecDifferences {
context := Request
if diff.DifferenceLocation.Response > 0 {
context = Response
}
diff.Compatibility = getCompatibilityForChange(diff.Code, context)
return append(sd, diff)
}
// ReportCompatibility lists and spec
func (sd *SpecDifferences) ReportCompatibility() error {
breakingCount := sd.BreakingChangeCount()
if breakingCount > 0 {
fmt.Printf("\nBREAKING CHANGES:\n=================\n")
sd.reportChanges(Breaking)
return fmt.Errorf("compatibility Test FAILED: %d Breaking changes detected", breakingCount)
}
log.Printf("Compatibility test OK. No breaking changes identified.")
return nil
}
func (sd SpecDifferences) reportChanges(compat Compatibility) {
toReportList := []string{}
for _, diff := range sd {
if diff.Compatibility == compat {
toReportList = append(toReportList, diff.String())
}
}
sort.Slice(toReportList, func(i, j int) bool {
return toReportList[i] < toReportList[j]
})
for _, eachDiff := range toReportList {
fmt.Println(eachDiff)
}
}
// ReportAllDiffs lists all the diffs between two specs
func (sd SpecDifferences) ReportAllDiffs(fmtJSON bool) error {
if fmtJSON {
b, err := JSONMarshal(sd)
if err != nil {
log.Fatalf("Couldn't print results: %v", err)
}
pretty, err := prettyprint(b)
if err != nil {
log.Fatalf("Couldn't print results: %v", err)
}
fmt.Println(string(pretty))
return nil
}
numDiffs := len(sd)
if numDiffs == 0 {
fmt.Println("No changes identified")
return nil
}
if numDiffs != sd.BreakingChangeCount() {
fmt.Println("NON-BREAKING CHANGES:\n=====================")
sd.reportChanges(NonBreaking)
}
return sd.ReportCompatibility()
}

View File

@ -0,0 +1,170 @@
package diff
import (
"fmt"
"github.com/go-openapi/spec"
)
func forItems(items *spec.Items) *spec.Schema {
if items == nil {
return nil
}
valids := items.CommonValidations
schema := spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{items.SimpleSchema.Type},
Format: items.SimpleSchema.Format,
Maximum: valids.Maximum,
ExclusiveMaximum: valids.ExclusiveMaximum,
Minimum: valids.Minimum,
ExclusiveMinimum: valids.ExclusiveMinimum,
MaxLength: valids.MaxLength,
MinLength: valids.MinLength,
Pattern: valids.Pattern,
MaxItems: valids.MaxItems,
MinItems: valids.MinItems,
UniqueItems: valids.UniqueItems,
MultipleOf: valids.MultipleOf,
Enum: valids.Enum,
},
}
return &schema
}
func forParam(param spec.Parameter) spec.SchemaProps {
return spec.SchemaProps{
Type: []string{param.Type},
Format: param.Format,
Items: &spec.SchemaOrArray{Schema: forItems(param.Items)},
Maximum: param.Maximum,
ExclusiveMaximum: param.ExclusiveMaximum,
Minimum: param.Minimum,
ExclusiveMinimum: param.ExclusiveMinimum,
MaxLength: param.MaxLength,
MinLength: param.MinLength,
Pattern: param.Pattern,
MaxItems: param.MaxItems,
MinItems: param.MinItems,
UniqueItems: param.UniqueItems,
MultipleOf: param.MultipleOf,
Enum: param.Enum,
}
}
// OperationMap saves indexing operations in PathItems individually
type OperationMap map[string]*spec.Operation
func toMap(item *spec.PathItem) OperationMap {
m := make(OperationMap)
if item.Post != nil {
m["post"] = item.Post
}
if item.Get != nil {
m["get"] = item.Get
}
if item.Put != nil {
m["put"] = item.Put
}
if item.Patch != nil {
m["patch"] = item.Patch
}
if item.Head != nil {
m["head"] = item.Head
}
if item.Options != nil {
m["options"] = item.Options
}
if item.Delete != nil {
m["delete"] = item.Delete
}
return m
}
func getURLMethodsFor(spec *spec.Swagger) URLMethods {
returnURLMethods := URLMethods{}
for url, eachPath := range spec.Paths.Paths {
eachPath := eachPath
opsMap := toMap(&eachPath)
for method, op := range opsMap {
returnURLMethods[URLMethod{url, method}] = &PathItemOp{&eachPath, op}
}
}
return returnURLMethods
}
func sliceToStrMap(elements []string) map[string]bool {
elementMap := make(map[string]bool)
for _, s := range elements {
elementMap[s] = true
}
return elementMap
}
func isStringType(typeName string) bool {
return typeName == "string" || typeName == "password"
}
const objType = "obj"
func getTypeHierarchyChange(type1, type2 string) TypeDiff {
if type1 == type2 {
return TypeDiff{Change: NoChangeDetected, Description: ""}
}
fromType := type1
if fromType == "" {
fromType = objType
}
toType := type2
if toType == "" {
toType = objType
}
diffDescription := fmt.Sprintf("%s -> %s", fromType, toType)
if isStringType(type1) && !isStringType(type2) {
return TypeDiff{Change: NarrowedType, Description: diffDescription}
}
if !isStringType(type1) && isStringType(type2) {
return TypeDiff{Change: WidenedType, Description: diffDescription}
}
type1Wideness, type1IsNumeric := numberWideness[type1]
type2Wideness, type2IsNumeric := numberWideness[type2]
if type1IsNumeric && type2IsNumeric {
if type1Wideness == type2Wideness {
return TypeDiff{Change: ChangedToCompatibleType, Description: diffDescription}
}
if type1Wideness > type2Wideness {
return TypeDiff{Change: NarrowedType, Description: diffDescription}
}
if type1Wideness < type2Wideness {
return TypeDiff{Change: WidenedType, Description: diffDescription}
}
}
return TypeDiff{Change: ChangedType, Description: diffDescription}
}
func compareFloatValues(fieldName string, val1 *float64, val2 *float64, ifGreaterCode SpecChangeCode, ifLessCode SpecChangeCode) TypeDiff {
if val1 != nil && val2 != nil {
if *val2 > *val1 {
return TypeDiff{Change: ifGreaterCode, Description: fmt.Sprintf("%s %f->%f", fieldName, *val1, *val2)}
}
if *val2 < *val1 {
return TypeDiff{Change: ifLessCode, Description: fmt.Sprintf("%s %f->%f", fieldName, *val1, *val2)}
}
}
return TypeDiff{Change: NoChangeDetected, Description: ""}
}
func compareIntValues(fieldName string, val1 *int64, val2 *int64, ifGreaterCode SpecChangeCode, ifLessCode SpecChangeCode) TypeDiff {
if val1 != nil && val2 != nil {
if *val2 > *val1 {
return TypeDiff{Change: ifGreaterCode, Description: fmt.Sprintf("%s %d->%d", fieldName, *val1, *val2)}
}
if *val2 < *val1 {
return TypeDiff{Change: ifLessCode, Description: fmt.Sprintf("%s %d->%d", fieldName, *val1, *val2)}
}
}
return TypeDiff{Change: NoChangeDetected, Description: ""}
}

View File

@ -0,0 +1,73 @@
package commands
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"github.com/go-openapi/loads"
"github.com/go-openapi/spec"
"github.com/go-openapi/swag"
flags "github.com/jessevdk/go-flags"
yaml "gopkg.in/yaml.v2"
)
// ExpandSpec is a command that expands the $refs in a swagger document.
//
// There are no specific options for this expansion.
type ExpandSpec struct {
Compact bool `long:"compact" description:"applies to JSON formatted specs. When present, doesn't prettify the json"`
Output flags.Filename `long:"output" short:"o" description:"the file to write to"`
Format string `long:"format" description:"the format for the spec document" default:"json" choice:"yaml" choice:"json"`
}
// Execute expands the spec
func (c *ExpandSpec) Execute(args []string) error {
if len(args) != 1 {
return errors.New("expand command requires the single swagger document url to be specified")
}
swaggerDoc := args[0]
specDoc, err := loads.Spec(swaggerDoc)
if err != nil {
return err
}
exp, err := specDoc.Expanded()
if err != nil {
return err
}
return writeToFile(exp.Spec(), !c.Compact, c.Format, string(c.Output))
}
func writeToFile(swspec *spec.Swagger, pretty bool, format string, output string) error {
var b []byte
var err error
asJSON := format == "json"
if pretty && asJSON {
b, err = json.MarshalIndent(swspec, "", " ")
} else if asJSON {
b, err = json.Marshal(swspec)
} else {
// marshals as YAML
b, err = json.Marshal(swspec)
if err == nil {
d, ery := swag.BytesToYAMLDoc(b)
if ery != nil {
return ery
}
b, err = yaml.Marshal(d)
}
}
if err != nil {
return err
}
if output == "" {
fmt.Println(string(b))
return nil
}
return ioutil.WriteFile(output, b, 0644)
}

View File

@ -0,0 +1,48 @@
package commands
import (
"errors"
"github.com/go-openapi/analysis"
"github.com/go-openapi/loads"
"github.com/go-swagger/go-swagger/cmd/swagger/commands/generate"
flags "github.com/jessevdk/go-flags"
)
// FlattenSpec is a command that flattens a swagger document
// which will expand the remote references in a spec and move inline schemas to definitions
// after flattening there are no complex inlined anymore
type FlattenSpec struct {
Compact bool `long:"compact" description:"applies to JSON formatted specs. When present, doesn't prettify the json"`
Output flags.Filename `long:"output" short:"o" description:"the file to write to"`
Format string `long:"format" description:"the format for the spec document" default:"json" choice:"yaml" choice:"json"`
generate.FlattenCmdOptions
}
// Execute flattens the spec
func (c *FlattenSpec) Execute(args []string) error {
if len(args) != 1 {
return errors.New("flatten command requires the single swagger document url to be specified")
}
swaggerDoc := args[0]
specDoc, err := loads.Spec(swaggerDoc)
if err != nil {
return err
}
flattenOpts := c.FlattenCmdOptions.SetFlattenOptions(&analysis.FlattenOpts{
// defaults
Minimal: true,
Verbose: true,
Expand: false,
RemoveUnused: false,
})
flattenOpts.BasePath = specDoc.SpecFilePath()
flattenOpts.Spec = analysis.New(specDoc.Spec())
if err := analysis.Flatten(*flattenOpts); err != nil {
return err
}
return writeToFile(specDoc.Spec(), !c.Compact, c.Format, string(c.Output))
}

View File

@ -0,0 +1,27 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import "github.com/go-swagger/go-swagger/cmd/swagger/commands/generate"
// Generate command to group all generator commands together
type Generate struct {
Model *generate.Model `command:"model"`
Operation *generate.Operation `command:"operation"`
Support *generate.Support `command:"support"`
Server *generate.Server `command:"server"`
Spec *generate.SpecFile `command:"spec"`
Client *generate.Client `command:"client"`
}

View File

@ -0,0 +1,92 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package generate
import (
"log"
"github.com/go-swagger/go-swagger/generator"
)
// Client the command to generate a swagger client
type Client struct {
shared
Name string `long:"name" short:"A" description:"the name of the application, defaults to a mangled value of info.title"`
Operations []string `long:"operation" short:"O" description:"specify an operation to include, repeat for multiple"`
Tags []string `long:"tags" description:"the tags to include, if not specified defaults to all"`
Principal string `long:"principal" short:"P" description:"the model to use for the security principal"`
Models []string `long:"model" short:"M" description:"specify a model to include, repeat for multiple"`
DefaultScheme string `long:"default-scheme" description:"the default scheme for this client" default:"http"`
DefaultProduces string `long:"default-produces" description:"the default mime type that API operations produce" default:"application/json"`
SkipModels bool `long:"skip-models" description:"no models will be generated when this flag is specified"`
SkipOperations bool `long:"skip-operations" description:"no operations will be generated when this flag is specified"`
DumpData bool `long:"dump-data" description:"when present dumps the json for the template generator instead of generating files"`
SkipValidation bool `long:"skip-validation" description:"skips validation of spec prior to generation"`
}
func (c *Client) getOpts() (*generator.GenOpts, error) {
return &generator.GenOpts{
Spec: string(c.Spec),
Target: string(c.Target),
APIPackage: c.APIPackage,
ModelPackage: c.ModelPackage,
ServerPackage: c.ServerPackage,
ClientPackage: c.ClientPackage,
Principal: c.Principal,
DefaultScheme: c.DefaultScheme,
DefaultProduces: c.DefaultProduces,
IncludeModel: !c.SkipModels,
IncludeValidator: !c.SkipModels,
IncludeHandler: !c.SkipOperations,
IncludeParameters: !c.SkipOperations,
IncludeResponses: !c.SkipOperations,
ValidateSpec: !c.SkipValidation,
Tags: c.Tags,
IncludeSupport: true,
Template: c.Template,
TemplateDir: string(c.TemplateDir),
DumpData: c.DumpData,
ExistingModels: c.ExistingModels,
IsClient: true,
}, nil
}
func (c *Client) getShared() *shared {
return &c.shared
}
func (c *Client) generate(opts *generator.GenOpts) error {
return generator.GenerateClient(c.Name, c.Models, c.Operations, opts)
}
func (c *Client) log(rp string) {
log.Printf(`Generation completed!
For this generation to compile you need to have some packages in your GOPATH:
* github.com/go-openapi/errors
* github.com/go-openapi/runtime
* github.com/go-openapi/runtime/client
* github.com/go-openapi/strfmt
You can get these now with: go get -u -f %s/...
`, rp)
}
// Execute runs this command
func (c *Client) Execute(args []string) error {
return createSwagger(c)
}

View File

@ -0,0 +1,16 @@
package generate
import (
"github.com/go-swagger/go-swagger/generator"
)
// contribOptionsOverride gives contributed templates the ability to override the options if they need
func contribOptionsOverride(opts *generator.GenOpts) {
switch opts.Template {
case "stratoscale":
// Stratoscale template needs to regenerate the configureapi on every run.
opts.RegenerateConfigureAPI = true
// It also does not use the main.go
opts.IncludeMain = false
}
}

View File

@ -0,0 +1,53 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package generate
import (
"errors"
"log"
)
// Model the generate model file command
type Model struct {
shared
Name []string `long:"name" short:"n" description:"the model to generate"`
NoStruct bool `long:"skip-struct" description:"when present will not generate the model struct"`
DumpData bool `long:"dump-data" description:"when present dumps the json for the template generator instead of generating files"`
SkipValidation bool `long:"skip-validation" description:"skips validation of spec prior to generation"`
}
// Execute generates a model file
func (m *Model) Execute(args []string) error {
if m.DumpData && len(m.Name) > 1 {
return errors.New("only 1 model at a time is supported for dumping data")
}
if m.ExistingModels != "" {
log.Println("warning: Ignoring existing-models flag when generating models.")
}
s := &Server{
shared: m.shared,
Models: m.Name,
DumpData: m.DumpData,
ExcludeMain: true,
ExcludeSpec: true,
SkipSupport: true,
SkipOperations: true,
SkipModels: m.NoStruct,
SkipValidation: m.SkipValidation,
}
return s.Execute(args)
}

View File

@ -0,0 +1,87 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package generate
import (
"errors"
"log"
"github.com/go-swagger/go-swagger/generator"
)
// Operation the generate operation files command
type Operation struct {
shared
Name []string `long:"name" short:"n" required:"true" description:"the operations to generate, repeat for multiple"`
Tags []string `long:"tags" description:"the tags to include, if not specified defaults to all"`
Principal string `short:"P" long:"principal" description:"the model to use for the security principal"`
DefaultScheme string `long:"default-scheme" description:"the default scheme for this API" default:"http"`
NoHandler bool `long:"skip-handler" description:"when present will not generate an operation handler"`
NoStruct bool `long:"skip-parameters" description:"when present will not generate the parameter model struct"`
NoResponses bool `long:"skip-responses" description:"when present will not generate the response model struct"`
NoURLBuilder bool `long:"skip-url-builder" description:"when present will not generate a URL builder"`
DumpData bool `long:"dump-data" description:"when present dumps the json for the template generator instead of generating files"`
SkipValidation bool `long:"skip-validation" description:"skips validation of spec prior to generation"`
}
func (o *Operation) getOpts() (*generator.GenOpts, error) {
return &generator.GenOpts{
Spec: string(o.Spec),
Target: string(o.Target),
APIPackage: o.APIPackage,
ModelPackage: o.ModelPackage,
ServerPackage: o.ServerPackage,
ClientPackage: o.ClientPackage,
Principal: o.Principal,
DumpData: o.DumpData,
DefaultScheme: o.DefaultScheme,
TemplateDir: string(o.TemplateDir),
IncludeHandler: !o.NoHandler,
IncludeResponses: !o.NoResponses,
IncludeParameters: !o.NoStruct,
IncludeURLBuilder: !o.NoURLBuilder,
Tags: o.Tags,
ValidateSpec: !o.SkipValidation,
}, nil
}
func (o *Operation) getShared() *shared {
return &o.shared
}
func (o *Operation) generate(opts *generator.GenOpts) error {
return generator.GenerateServerOperation(o.Name, opts)
}
func (o *Operation) log(rp string) {
log.Printf(`Generation completed!
For this generation to compile you need to have some packages in your GOPATH:
* github.com/go-openapi/runtime
You can get these now with: go get -u -f %s/...
`, rp)
}
// Execute generates a model file
func (o *Operation) Execute(args []string) error {
if o.DumpData && len(o.Name) > 1 {
return errors.New("only 1 operation at a time is supported for dumping data")
}
return createSwagger(o)
}

View File

@ -0,0 +1,119 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package generate
import (
"log"
"strings"
"github.com/go-swagger/go-swagger/generator"
)
// Server the command to generate an entire server application
type Server struct {
shared
Name string `long:"name" short:"A" description:"the name of the application, defaults to a mangled value of info.title"`
Operations []string `long:"operation" short:"O" description:"specify an operation to include, repeat for multiple"`
Tags []string `long:"tags" description:"the tags to include, if not specified defaults to all"`
Principal string `long:"principal" short:"P" description:"the model to use for the security principal"`
DefaultScheme string `long:"default-scheme" description:"the default scheme for this API" default:"http"`
Models []string `long:"model" short:"M" description:"specify a model to include, repeat for multiple"`
SkipModels bool `long:"skip-models" description:"no models will be generated when this flag is specified"`
SkipOperations bool `long:"skip-operations" description:"no operations will be generated when this flag is specified"`
SkipSupport bool `long:"skip-support" description:"no supporting files will be generated when this flag is specified"`
ExcludeMain bool `long:"exclude-main" description:"exclude main function, so just generate the library"`
ExcludeSpec bool `long:"exclude-spec" description:"don't embed the swagger specification"`
WithContext bool `long:"with-context" description:"handlers get a context as first arg (deprecated)"`
DumpData bool `long:"dump-data" description:"when present dumps the json for the template generator instead of generating files"`
FlagStrategy string `long:"flag-strategy" description:"the strategy to provide flags for the server" default:"go-flags" choice:"go-flags" choice:"pflag"`
CompatibilityMode string `long:"compatibility-mode" description:"the compatibility mode for the tls server" default:"modern" choice:"modern" choice:"intermediate"`
SkipValidation bool `long:"skip-validation" description:"skips validation of spec prior to generation"`
RegenerateConfigureAPI bool `long:"regenerate-configureapi" description:"Force regeneration of configureapi.go"`
KeepSpecOrder bool `long:"keep-spec-order" description:"Keep schema properties order identical to spec file"`
StrictAdditionalProperties bool `long:"strict-additional-properties" description:"disallow extra properties when additionalProperties is set to false"`
}
func (s *Server) getOpts() (*generator.GenOpts, error) {
// warning: deprecation
if s.WithContext {
log.Printf("warning: deprecated option --with-context is ignored")
}
return &generator.GenOpts{
Spec: string(s.Spec),
Target: string(s.Target),
APIPackage: s.APIPackage,
ModelPackage: s.ModelPackage,
ServerPackage: s.ServerPackage,
ClientPackage: s.ClientPackage,
Principal: s.Principal,
DefaultScheme: s.DefaultScheme,
IncludeModel: !s.SkipModels,
IncludeValidator: !s.SkipModels,
IncludeHandler: !s.SkipOperations,
IncludeParameters: !s.SkipOperations,
IncludeResponses: !s.SkipOperations,
IncludeURLBuilder: !s.SkipOperations,
IncludeMain: !s.ExcludeMain,
IncludeSupport: !s.SkipSupport,
PropertiesSpecOrder: s.KeepSpecOrder,
ValidateSpec: !s.SkipValidation,
ExcludeSpec: s.ExcludeSpec,
StrictAdditionalProperties: s.StrictAdditionalProperties,
Template: s.Template,
RegenerateConfigureAPI: s.RegenerateConfigureAPI,
TemplateDir: string(s.TemplateDir),
DumpData: s.DumpData,
Models: s.Models,
Operations: s.Operations,
Tags: s.Tags,
Name: s.Name,
FlagStrategy: s.FlagStrategy,
CompatibilityMode: s.CompatibilityMode,
ExistingModels: s.ExistingModels,
}, nil
}
func (s *Server) getShared() *shared {
return &s.shared
}
func (s *Server) generate(opts *generator.GenOpts) error {
return generator.GenerateServer(s.Name, s.Models, s.Operations, opts)
}
func (s *Server) log(rp string) {
var flagsPackage string
if strings.HasPrefix(s.FlagStrategy, "pflag") {
flagsPackage = "github.com/spf13/pflag"
} else {
flagsPackage = "github.com/jessevdk/go-flags"
}
log.Printf(`Generation completed!
For this generation to compile you need to have some packages in your GOPATH:
* github.com/go-openapi/runtime
* `+flagsPackage+`
You can get these now with: go get -u -f %s/...
`, rp)
}
// Execute runs this command
func (s *Server) Execute(args []string) error {
return createSwagger(s)
}

View File

@ -0,0 +1,213 @@
package generate
import (
"io/ioutil"
"log"
"os"
"path/filepath"
"github.com/go-openapi/analysis"
"github.com/go-openapi/swag"
"github.com/go-swagger/go-swagger/generator"
flags "github.com/jessevdk/go-flags"
"github.com/spf13/viper"
)
// FlattenCmdOptions determines options to the flatten spec preprocessing
type FlattenCmdOptions struct {
WithExpand bool `long:"with-expand" description:"expands all $ref's in spec prior to generation (shorthand to --with-flatten=expand)"`
WithFlatten []string `long:"with-flatten" description:"flattens all $ref's in spec prior to generation" choice:"minimal" choice:"full" choice:"expand" choice:"verbose" choice:"noverbose" choice:"remove-unused" default:"minimal" default:"verbose"`
}
// SetFlattenOptions builds flatten options from command line args
func (f *FlattenCmdOptions) SetFlattenOptions(dflt *analysis.FlattenOpts) (res *analysis.FlattenOpts) {
res = &analysis.FlattenOpts{}
if dflt != nil {
*res = *dflt
}
if f == nil {
return
}
verboseIsSet := false
minimalIsSet := false
//removeUnusedIsSet := false
expandIsSet := false
if f.WithExpand {
res.Expand = true
expandIsSet = true
}
for _, opt := range f.WithFlatten {
if opt == "verbose" {
res.Verbose = true
verboseIsSet = true
}
if opt == "noverbose" && !verboseIsSet {
// verbose flag takes precedence
res.Verbose = false
verboseIsSet = true
}
if opt == "remove-unused" {
res.RemoveUnused = true
//removeUnusedIsSet = true
}
if opt == "expand" {
res.Expand = true
expandIsSet = true
}
if opt == "full" && !minimalIsSet && !expandIsSet {
// minimal flag takes precedence
res.Minimal = false
minimalIsSet = true
}
if opt == "minimal" && !expandIsSet {
// expand flag takes precedence
res.Minimal = true
minimalIsSet = true
}
}
return
}
type shared struct {
Spec flags.Filename `long:"spec" short:"f" description:"the spec file to use (default swagger.{json,yml,yaml})"`
APIPackage string `long:"api-package" short:"a" description:"the package to save the operations" default:"operations"`
ModelPackage string `long:"model-package" short:"m" description:"the package to save the models" default:"models"`
ServerPackage string `long:"server-package" short:"s" description:"the package to save the server specific code" default:"restapi"`
ClientPackage string `long:"client-package" short:"c" description:"the package to save the client specific code" default:"client"`
Target flags.Filename `long:"target" short:"t" default:"./" description:"the base directory for generating the files"`
Template string `long:"template" description:"Load contributed templates" choice:"stratoscale"`
TemplateDir flags.Filename `long:"template-dir" short:"T" description:"alternative template override directory"`
ConfigFile flags.Filename `long:"config-file" short:"C" description:"configuration file to use for overriding template options"`
CopyrightFile flags.Filename `long:"copyright-file" short:"r" description:"copyright file used to add copyright header"`
ExistingModels string `long:"existing-models" description:"use pre-generated models e.g. github.com/foobar/model"`
AdditionalInitialisms []string `long:"additional-initialism" description:"consecutive capitals that should be considered intialisms"`
FlattenCmdOptions
}
type sharedCommand interface {
getOpts() (*generator.GenOpts, error)
getShared() *shared
getConfigFile() flags.Filename
getAdditionalInitialisms() []string
generate(*generator.GenOpts) error
log(string)
}
func (s *shared) getConfigFile() flags.Filename {
return s.ConfigFile
}
func (s *shared) getAdditionalInitialisms() []string {
return s.AdditionalInitialisms
}
func (s *shared) setCopyright() (string, error) {
var copyrightstr string
copyrightfile := string(s.CopyrightFile)
if copyrightfile != "" {
//Read the Copyright from file path in opts
bytebuffer, err := ioutil.ReadFile(copyrightfile)
if err != nil {
return "", err
}
copyrightstr = string(bytebuffer)
} else {
copyrightstr = ""
}
return copyrightstr, nil
}
func createSwagger(s sharedCommand) error {
cfg, erc := readConfig(string(s.getConfigFile()))
if erc != nil {
return erc
}
setDebug(cfg)
opts, ero := s.getOpts()
if ero != nil {
return ero
}
if opts.Template != "" {
contribOptionsOverride(opts)
}
if err := opts.EnsureDefaults(); err != nil {
return err
}
if err := configureOptsFromConfig(cfg, opts); err != nil {
return err
}
swag.AddInitialisms(s.getAdditionalInitialisms()...)
if sharedOpts := s.getShared(); sharedOpts != nil {
// process shared options
opts.FlattenOpts = sharedOpts.FlattenCmdOptions.SetFlattenOptions(opts.FlattenOpts)
copyrightStr, erc := sharedOpts.setCopyright()
if erc != nil {
return erc
}
opts.Copyright = copyrightStr
}
if err := s.generate(opts); err != nil {
return err
}
basepath, era := filepath.Abs(".")
if era != nil {
return era
}
targetAbs, err := filepath.Abs(opts.Target)
if err != nil {
return err
}
rp, err := filepath.Rel(basepath, targetAbs)
if err != nil {
return err
}
s.log(rp)
return nil
}
func readConfig(filename string) (*viper.Viper, error) {
if filename == "" {
return nil, nil
}
abspath, err := filepath.Abs(filename)
if err != nil {
return nil, err
}
log.Println("trying to read config from", abspath)
return generator.ReadConfig(abspath)
}
func configureOptsFromConfig(cfg *viper.Viper, opts *generator.GenOpts) error {
if cfg == nil {
return nil
}
var def generator.LanguageDefinition
if err := cfg.Unmarshal(&def); err != nil {
return err
}
return def.ConfigureOpts(opts)
}
func setDebug(cfg *viper.Viper) {
if os.Getenv("DEBUG") != "" || os.Getenv("SWAGGER_DEBUG") != "" {
if cfg != nil {
cfg.Debug()
} else {
log.Println("NO config read")
}
}
}

View File

@ -0,0 +1,125 @@
//+build !go1.11
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package generate
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/go-openapi/loads"
"github.com/go-openapi/spec"
"github.com/go-swagger/go-swagger/scan"
"github.com/jessevdk/go-flags"
"gopkg.in/yaml.v2"
)
// SpecFile command to generate a swagger spec from a go application
type SpecFile struct {
BasePath string `long:"base-path" short:"b" description:"the base path to use" default:"."`
BuildTags string `long:"tags" short:"t" description:"build tags" default:""`
ScanModels bool `long:"scan-models" short:"m" description:"includes models that were annotated with 'swagger:model'"`
Compact bool `long:"compact" description:"when present, doesn't prettify the json"`
Output flags.Filename `long:"output" short:"o" description:"the file to write to"`
Input flags.Filename `long:"input" short:"i" description:"the file to use as input"`
Include []string `long:"include" short:"c" description:"include packages matching pattern"`
Exclude []string `long:"exclude" short:"x" description:"exclude packages matching pattern"`
IncludeTags []string `long:"include-tag" short:"" description:"include routes having specified tags (can be specified many times)"`
ExcludeTags []string `long:"exclude-tag" short:"" description:"exclude routes having specified tags (can be specified many times)"`
}
// Execute runs this command
func (s *SpecFile) Execute(args []string) error {
input, err := loadSpec(string(s.Input))
if err != nil {
return err
}
var opts scan.Opts
opts.BasePath = s.BasePath
opts.Input = input
opts.ScanModels = s.ScanModels
opts.BuildTags = s.BuildTags
opts.Include = s.Include
opts.Exclude = s.Exclude
opts.IncludeTags = s.IncludeTags
opts.ExcludeTags = s.ExcludeTags
swspec, err := scan.Application(opts)
if err != nil {
return err
}
return writeToFile(swspec, !s.Compact, string(s.Output))
}
func loadSpec(input string) (*spec.Swagger, error) {
if fi, err := os.Stat(input); err == nil {
if fi.IsDir() {
return nil, fmt.Errorf("expected %q to be a file not a directory", input)
}
sp, err := loads.Spec(input)
if err != nil {
return nil, err
}
return sp.Spec(), nil
}
return nil, nil
}
func writeToFile(swspec *spec.Swagger, pretty bool, output string) error {
var b []byte
var err error
if strings.HasSuffix(output, "yml") || strings.HasSuffix(output, "yaml") {
b, err = marshalToYAMLFormat(swspec)
} else {
b, err = marshalToJSONFormat(swspec, pretty)
}
if err != nil {
return err
}
if output == "" {
fmt.Println(string(b))
return nil
}
return ioutil.WriteFile(output, b, 0644)
}
func marshalToJSONFormat(swspec *spec.Swagger, pretty bool) ([]byte, error) {
if pretty {
return json.MarshalIndent(swspec, "", " ")
}
return json.Marshal(swspec)
}
func marshalToYAMLFormat(swspec *spec.Swagger) ([]byte, error) {
b, err := json.Marshal(swspec)
if err != nil {
return nil, err
}
var jsonObj interface{}
if err := yaml.Unmarshal(b, &jsonObj); err != nil {
return nil, err
}
return yaml.Marshal(jsonObj)
}

View File

@ -0,0 +1,119 @@
// +build go1.11
package generate
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/go-swagger/go-swagger/codescan"
"github.com/go-openapi/loads"
"github.com/go-openapi/spec"
"github.com/jessevdk/go-flags"
"gopkg.in/yaml.v2"
)
// SpecFile command to generate a swagger spec from a go application
type SpecFile struct {
WorkDir string `long:"work-dir" short:"w" description:"the base path to use" default:"."`
BuildTags string `long:"tags" short:"t" description:"build tags" default:""`
ScanModels bool `long:"scan-models" short:"m" description:"includes models that were annotated with 'swagger:model'"`
Compact bool `long:"compact" description:"when present, doesn't prettify the json"`
Output flags.Filename `long:"output" short:"o" description:"the file to write to"`
Input flags.Filename `long:"input" short:"i" description:"the file to use as input"`
Include []string `long:"include" short:"c" description:"include packages matching pattern"`
Exclude []string `long:"exclude" short:"x" description:"exclude packages matching pattern"`
IncludeTags []string `long:"include-tag" short:"" description:"include routes having specified tags (can be specified many times)"`
ExcludeTags []string `long:"exclude-tag" short:"" description:"exclude routes having specified tags (can be specified many times)"`
ExcludeDeps bool `long:"exclude-deps" short:"" description:"exclude all dependencies of project"`
}
// Execute runs this command
func (s *SpecFile) Execute(args []string) error {
if len(args) == 0 { // by default consider all the paths under the working directory
args = []string{"./..."}
}
input, err := loadSpec(string(s.Input))
if err != nil {
return err
}
var opts codescan.Options
opts.Packages = args
opts.WorkDir = s.WorkDir
opts.InputSpec = input
opts.ScanModels = s.ScanModels
opts.BuildTags = s.BuildTags
opts.Include = s.Include
opts.Exclude = s.Exclude
opts.IncludeTags = s.IncludeTags
opts.ExcludeTags = s.ExcludeTags
opts.ExcludeDeps = s.ExcludeDeps
swspec, err := codescan.Run(&opts)
if err != nil {
return err
}
return writeToFile(swspec, !s.Compact, string(s.Output))
}
func loadSpec(input string) (*spec.Swagger, error) {
if fi, err := os.Stat(input); err == nil {
if fi.IsDir() {
return nil, fmt.Errorf("expected %q to be a file not a directory", input)
}
sp, err := loads.Spec(input)
if err != nil {
return nil, err
}
return sp.Spec(), nil
}
return nil, nil
}
func writeToFile(swspec *spec.Swagger, pretty bool, output string) error {
var b []byte
var err error
if strings.HasSuffix(output, "yml") || strings.HasSuffix(output, "yaml") {
b, err = marshalToYAMLFormat(swspec)
} else {
b, err = marshalToJSONFormat(swspec, pretty)
}
if err != nil {
return err
}
if output == "" {
fmt.Println(string(b))
return nil
}
return ioutil.WriteFile(output, b, 0644)
}
func marshalToJSONFormat(swspec *spec.Swagger, pretty bool) ([]byte, error) {
if pretty {
return json.MarshalIndent(swspec, "", " ")
}
return json.Marshal(swspec)
}
func marshalToYAMLFormat(swspec *spec.Swagger) ([]byte, error) {
b, err := json.Marshal(swspec)
if err != nil {
return nil, err
}
var jsonObj interface{}
if err := yaml.Unmarshal(b, &jsonObj); err != nil {
return nil, err
}
return yaml.Marshal(jsonObj)
}

View File

@ -0,0 +1,76 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package generate
import (
"log"
"github.com/go-swagger/go-swagger/generator"
)
// Support generates the supporting files
type Support struct {
shared
Name string `long:"name" short:"A" description:"the name of the application, defaults to a mangled value of info.title"`
Operations []string `long:"operation" short:"O" description:"specify an operation to include, repeat for multiple"`
Principal string `long:"principal" description:"the model to use for the security principal"`
Models []string `long:"model" short:"M" description:"specify a model to include, repeat for multiple"`
DumpData bool `long:"dump-data" description:"when present dumps the json for the template generator instead of generating files"`
DefaultScheme string `long:"default-scheme" description:"the default scheme for this API" default:"http"`
}
func (s *Support) getOpts() (*generator.GenOpts, error) {
return &generator.GenOpts{
Spec: string(s.Spec),
Target: string(s.Target),
APIPackage: s.APIPackage,
ModelPackage: s.ModelPackage,
ServerPackage: s.ServerPackage,
ClientPackage: s.ClientPackage,
Principal: s.Principal,
DumpData: s.DumpData,
DefaultScheme: s.DefaultScheme,
Template: s.Template,
TemplateDir: string(s.TemplateDir),
}, nil
}
func (s *Support) getShared() *shared {
return &s.shared
}
func (s *Support) generate(opts *generator.GenOpts) error {
return generator.GenerateSupport(s.Name, nil, nil, opts)
}
func (s *Support) log(rp string) {
log.Printf(`Generation completed!
For this generation to compile you need to have some packages in your vendor or GOPATH:
* github.com/go-openapi/runtime
* github.com/asaskevich/govalidator
* github.com/jessevdk/go-flags
* golang.org/x/net/context/ctxhttp
You can get these now with: go get -u -f %s/...
`, rp)
}
// Execute generates the supporting files file
func (s *Support) Execute(args []string) error {
return createSwagger(s)
}

View File

@ -0,0 +1,13 @@
package commands
import "github.com/go-swagger/go-swagger/cmd/swagger/commands/initcmd"
// InitCmd is a command namespace for initializing things like a swagger spec.
type InitCmd struct {
Model *initcmd.Spec `command:"spec"`
}
// Execute provides default empty implementation
func (i *InitCmd) Execute(args []string) error {
return nil
}

View File

@ -0,0 +1,111 @@
package initcmd
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"gopkg.in/yaml.v2"
"github.com/go-openapi/spec"
"github.com/go-openapi/swag"
)
// Spec a command struct for initializing a new swagger application.
type Spec struct {
Format string `long:"format" description:"the format for the spec document" default:"yaml" choice:"yaml" choice:"json"`
Title string `long:"title" description:"the title of the API"`
Description string `long:"description" description:"the description of the API"`
Version string `long:"version" description:"the version of the API" default:"0.1.0"`
Terms string `long:"terms" description:"the terms of services"`
Consumes []string `long:"consumes" description:"add a content type to the global consumes definitions, can repeat" default:"application/json"`
Produces []string `long:"produces" description:"add a content type to the global produces definitions, can repeat" default:"application/json"`
Schemes []string `long:"scheme" description:"add a scheme to the global schemes definition, can repeat" default:"http"`
Contact struct {
Name string `long:"contact.name" description:"name of the primary contact for the API"`
URL string `long:"contact.url" description:"url of the primary contact for the API"`
Email string `long:"contact.email" description:"email of the primary contact for the API"`
}
License struct {
Name string `long:"license.name" description:"name of the license for the API"`
URL string `long:"license.url" description:"url of the license for the API"`
}
}
// Execute this command
func (s *Spec) Execute(args []string) error {
targetPath := "."
if len(args) > 0 {
targetPath = args[0]
}
realPath, err := filepath.Abs(targetPath)
if err != nil {
return err
}
var file *os.File
switch s.Format {
case "json":
file, err = os.Create(filepath.Join(realPath, "swagger.json"))
if err != nil {
return err
}
case "yaml", "yml":
file, err = os.Create(filepath.Join(realPath, "swagger.yml"))
if err != nil {
return err
}
default:
return fmt.Errorf("invalid format: %s", s.Format)
}
defer file.Close()
log.Println("creating specification document in", filepath.Join(targetPath, file.Name()))
var doc spec.Swagger
info := new(spec.Info)
doc.Info = info
doc.Swagger = "2.0"
doc.Paths = new(spec.Paths)
doc.Definitions = make(spec.Definitions)
info.Title = s.Title
if info.Title == "" {
info.Title = swag.ToHumanNameTitle(filepath.Base(realPath))
}
info.Description = s.Description
info.Version = s.Version
info.TermsOfService = s.Terms
if s.Contact.Name != "" || s.Contact.Email != "" || s.Contact.URL != "" {
var contact spec.ContactInfo
contact.Name = s.Contact.Name
contact.Email = s.Contact.Email
contact.URL = s.Contact.URL
info.Contact = &contact
}
if s.License.Name != "" || s.License.URL != "" {
var license spec.License
license.Name = s.License.Name
license.URL = s.License.URL
info.License = &license
}
doc.Consumes = append(doc.Consumes, s.Consumes...)
doc.Produces = append(doc.Produces, s.Produces...)
doc.Schemes = append(doc.Schemes, s.Schemes...)
if s.Format == "json" {
enc := json.NewEncoder(file)
return enc.Encode(doc)
}
b, err := yaml.Marshal(swag.ToDynamicJSON(doc))
if err != nil {
return err
}
if _, err := file.Write(b); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,103 @@
package commands
import (
"errors"
"io"
"log"
"os"
"github.com/go-openapi/analysis"
"github.com/go-openapi/loads"
"github.com/go-openapi/spec"
flags "github.com/jessevdk/go-flags"
)
const (
// Output messages
nothingToDo = "Nothing to do. Need some swagger files to merge.\nUSAGE: swagger mixin [-c <expected#Collisions>] <primary-swagger-file> <mixin-swagger-file>..."
)
// MixinSpec holds command line flag definitions specific to the mixin
// command. The flags are defined using struct field tags with the
// "github.com/jessevdk/go-flags" format.
type MixinSpec struct {
ExpectedCollisionCount uint `short:"c" description:"expected # of rejected mixin paths, defs, etc due to existing key. Non-zero exit if does not match actual."`
Compact bool `long:"compact" description:"applies to JSON formatted specs. When present, doesn't prettify the json"`
Output flags.Filename `long:"output" short:"o" description:"the file to write to"`
Format string `long:"format" description:"the format for the spec document" default:"json" choice:"yaml" choice:"json"`
}
// Execute runs the mixin command which merges Swagger 2.0 specs into
// one spec
//
// Use cases include adding independently versioned metadata APIs to
// application APIs for microservices.
//
// Typically, multiple APIs to the same service instance is not a
// problem for client generation as you can create more than one
// client to the service from the same calling process (one for each
// API). However, merging clients can improve clarity of client code
// by having a single client to given service vs several.
//
// Server skeleton generation, ie generating the model & marshaling
// code, http server instance etc. from Swagger, becomes easier with a
// merged spec for some tools & target-languages. Server code
// generation tools that natively support hosting multiple specs in
// one server process will not need this tool.
func (c *MixinSpec) Execute(args []string) error {
if len(args) < 2 {
return errors.New(nothingToDo)
}
log.Printf("args[0] = %v\n", args[0])
log.Printf("args[1:] = %v\n", args[1:])
collisions, err := c.MixinFiles(args[0], args[1:], os.Stdout)
for _, warn := range collisions {
log.Println(warn)
}
if err != nil {
return err
}
if len(collisions) != int(c.ExpectedCollisionCount) {
if len(collisions) != 0 {
// use bash $? to get actual # collisions
// (but has to be non-zero)
os.Exit(len(collisions))
}
os.Exit(254)
}
return nil
}
// MixinFiles is a convenience function for Mixin that reads the given
// swagger files, adds the mixins to primary, calls
// FixEmptyResponseDescriptions on the primary, and writes the primary
// with mixins to the given writer in JSON. Returns the warning
// messages for collisions that occurred during mixin process and any
// error.
func (c *MixinSpec) MixinFiles(primaryFile string, mixinFiles []string, w io.Writer) ([]string, error) {
primaryDoc, err := loads.Spec(primaryFile)
if err != nil {
return nil, err
}
primary := primaryDoc.Spec()
var mixins []*spec.Swagger
for _, mixinFile := range mixinFiles {
mixin, lerr := loads.Spec(mixinFile)
if lerr != nil {
return nil, lerr
}
mixins = append(mixins, mixin.Spec())
}
collisions := analysis.Mixin(primary, mixins...)
analysis.FixEmptyResponseDescriptions(primary)
return collisions, writeToFile(primary, !c.Compact, c.Format, string(c.Output))
}

View File

@ -0,0 +1,107 @@
package commands
import (
"encoding/json"
"errors"
"fmt"
"log"
"net"
"net/http"
"net/url"
"path"
"strconv"
"github.com/go-openapi/loads"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/swag"
"github.com/gorilla/handlers"
"github.com/toqueteos/webbrowser"
)
// ServeCmd to serve a swagger spec with docs ui
type ServeCmd struct {
BasePath string `long:"base-path" description:"the base path to serve the spec and UI at"`
Flavor string `short:"F" long:"flavor" description:"the flavor of docs, can be swagger or redoc" default:"redoc" choice:"redoc" choice:"swagger"`
DocURL string `long:"doc-url" description:"override the url which takes a url query param to render the doc ui"`
NoOpen bool `long:"no-open" description:"when present won't open the the browser to show the url"`
NoUI bool `long:"no-ui" description:"when present, only the swagger spec will be served"`
Port int `long:"port" short:"p" description:"the port to serve this site" env:"PORT"`
Host string `long:"host" description:"the interface to serve this site, defaults to 0.0.0.0" env:"HOST"`
}
// Execute the serve command
func (s *ServeCmd) Execute(args []string) error {
if len(args) == 0 {
return errors.New("specify the spec to serve as argument to the serve command")
}
specDoc, err := loads.Spec(args[0])
if err != nil {
return err
}
b, err := json.MarshalIndent(specDoc.Spec(), "", " ")
if err != nil {
return err
}
basePath := s.BasePath
if basePath == "" {
basePath = "/"
}
listener, err := net.Listen("tcp4", net.JoinHostPort(s.Host, strconv.Itoa(s.Port)))
if err != nil {
return err
}
sh, sp, err := swag.SplitHostPort(listener.Addr().String())
if err != nil {
return err
}
if sh == "0.0.0.0" {
sh = "localhost"
}
visit := s.DocURL
handler := http.NotFoundHandler()
if !s.NoUI {
if s.Flavor == "redoc" {
handler = middleware.Redoc(middleware.RedocOpts{
BasePath: basePath,
SpecURL: path.Join(basePath, "swagger.json"),
Path: "docs",
}, handler)
visit = fmt.Sprintf("http://%s:%d%s", sh, sp, path.Join(basePath, "docs"))
} else if visit != "" || s.Flavor == "swagger" {
if visit == "" {
visit = "http://petstore.swagger.io/"
}
u, err := url.Parse(visit)
if err != nil {
return err
}
q := u.Query()
q.Add("url", fmt.Sprintf("http://%s:%d%s", sh, sp, path.Join(basePath, "swagger.json")))
u.RawQuery = q.Encode()
visit = u.String()
}
}
handler = handlers.CORS()(middleware.Spec(basePath, b, handler))
errFuture := make(chan error)
go func() {
docServer := new(http.Server)
docServer.SetKeepAlivesEnabled(true)
docServer.Handler = handler
errFuture <- docServer.Serve(listener)
}()
if !s.NoOpen && !s.NoUI {
err := webbrowser.Open(visit)
if err != nil {
return err
}
}
log.Println("serving docs at", visit)
return <-errFuture
}

View File

@ -0,0 +1,83 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"errors"
"fmt"
"log"
"github.com/go-openapi/loads"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/validate"
)
const (
// Output messages
missingArgMsg = "The validate command requires the swagger document url to be specified"
validSpecMsg = "\nThe swagger spec at %q is valid against swagger specification %s\n"
invalidSpecMsg = "\nThe swagger spec at %q is invalid against swagger specification %s.\nSee errors below:\n"
warningSpecMsg = "\nThe swagger spec at %q showed up some valid but possibly unwanted constructs."
)
// ValidateSpec is a command that validates a swagger document
// against the swagger specification
type ValidateSpec struct {
// SchemaURL string `long:"schema" description:"The schema url to use" default:"http://swagger.io/v2/schema.json"`
SkipWarnings bool `long:"skip-warnings" description:"when present will not show up warnings upon validation"`
StopOnError bool `long:"stop-on-error" description:"when present will not continue validation after critical errors are found"`
}
// Execute validates the spec
func (c *ValidateSpec) Execute(args []string) error {
if len(args) == 0 {
return errors.New(missingArgMsg)
}
swaggerDoc := args[0]
specDoc, err := loads.Spec(swaggerDoc)
if err != nil {
return err
}
// Attempts to report about all errors
validate.SetContinueOnErrors(!c.StopOnError)
v := validate.NewSpecValidator(specDoc.Schema(), strfmt.Default)
result, _ := v.Validate(specDoc) // returns fully detailed result with errors and warnings
if result.IsValid() {
log.Printf(validSpecMsg, swaggerDoc, specDoc.Version())
}
if result.HasWarnings() {
log.Printf(warningSpecMsg, swaggerDoc)
if !c.SkipWarnings {
log.Printf("See warnings below:\n")
for _, desc := range result.Warnings {
log.Printf("- WARNING: %s\n", desc.Error())
}
}
}
if result.HasErrors() {
str := fmt.Sprintf(invalidSpecMsg, swaggerDoc, specDoc.Version())
for _, desc := range result.Errors {
str += fmt.Sprintf("- %s\n", desc.Error())
}
return errors.New(str)
}
return nil
}

View File

@ -0,0 +1,26 @@
package commands
import "fmt"
var (
// Version for the swagger command
Version string
// Commit for the swagger command
Commit string
)
// PrintVersion the command
type PrintVersion struct {
}
// Execute this command
func (p *PrintVersion) Execute(args []string) error {
if Version == "" {
fmt.Println("dev")
return nil
}
fmt.Println("version:", Version)
fmt.Println("commit:", Commit)
return nil
}

View File

@ -0,0 +1,148 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"io/ioutil"
"log"
"os"
"github.com/go-openapi/loads"
"github.com/go-openapi/loads/fmts"
"github.com/go-swagger/go-swagger/cmd/swagger/commands"
flags "github.com/jessevdk/go-flags"
)
func init() {
loads.AddLoader(fmts.YAMLMatcher, fmts.YAMLDoc)
}
var (
// Debug is true when the SWAGGER_DEBUG env var is not empty
Debug = os.Getenv("SWAGGER_DEBUG") != ""
)
var opts struct {
// General options applicable to all commands
Quiet func() `long:"quiet" short:"q" description:"silence logs"`
LogFile func(string) `long:"log-output" description:"redirect logs to file" value-name:"LOG-FILE"`
// Version bool `long:"version" short:"v" description:"print the version of the command"`
}
func main() {
// TODO: reactivate 'defer catch all' once product is stable
// Recovering from internal panics
// Stack may be printed in Debug mode
// Need import "runtime/debug".
//defer func() {
// r := recover()
// if r != nil {
// log.Printf("Fatal error:", r)
// if Debug {
// debug.PrintStack()
// }
// os.Exit(1)
// }
//}()
parser := flags.NewParser(&opts, flags.Default)
parser.ShortDescription = "helps you keep your API well described"
parser.LongDescription = `
Swagger tries to support you as best as possible when building APIs.
It aims to represent the contract of your API with a language agnostic description of your application in json or yaml.
`
_, err := parser.AddCommand("validate", "validate the swagger document", "validate the provided swagger document against a swagger spec", &commands.ValidateSpec{})
if err != nil {
log.Fatal(err)
}
_, err = parser.AddCommand("init", "initialize a spec document", "initialize a swagger spec document", &commands.InitCmd{})
if err != nil {
log.Fatal(err)
}
_, err = parser.AddCommand("version", "print the version", "print the version of the swagger command", &commands.PrintVersion{})
if err != nil {
log.Fatal(err)
}
_, err = parser.AddCommand("serve", "serve spec and docs", "serve a spec and swagger or redoc documentation ui", &commands.ServeCmd{})
if err != nil {
log.Fatal(err)
}
_, err = parser.AddCommand("expand", "expand $ref fields in a swagger spec", "expands the $refs in a swagger document to inline schemas", &commands.ExpandSpec{})
if err != nil {
log.Fatal(err)
}
_, err = parser.AddCommand("flatten", "flattens a swagger document", "expand the remote references in a spec and move inline schemas to definitions, after flattening there are no complex inlined anymore", &commands.FlattenSpec{})
if err != nil {
log.Fatal(err)
}
_, err = parser.AddCommand("mixin", "merge swagger documents", "merge additional specs into first/primary spec by copying their paths and definitions", &commands.MixinSpec{})
if err != nil {
log.Fatal(err)
}
_, err = parser.AddCommand("diff", "diff swagger documents", "diff specs showing which changes will break existing clients", &commands.DiffCommand{})
if err != nil {
log.Fatal(err)
}
genpar, err := parser.AddCommand("generate", "generate go code", "generate go code for the swagger spec file", &commands.Generate{})
if err != nil {
log.Fatalln(err)
}
for _, cmd := range genpar.Commands() {
switch cmd.Name {
case "spec":
cmd.ShortDescription = "generate a swagger spec document from a go application"
cmd.LongDescription = cmd.ShortDescription
case "client":
cmd.ShortDescription = "generate all the files for a client library"
cmd.LongDescription = cmd.ShortDescription
case "server":
cmd.ShortDescription = "generate all the files for a server application"
cmd.LongDescription = cmd.ShortDescription
case "model":
cmd.ShortDescription = "generate one or more models from the swagger spec"
cmd.LongDescription = cmd.ShortDescription
case "support":
cmd.ShortDescription = "generate supporting files like the main function and the api builder"
cmd.LongDescription = cmd.ShortDescription
case "operation":
cmd.ShortDescription = "generate one or more server operations from the swagger spec"
cmd.LongDescription = cmd.ShortDescription
}
}
opts.Quiet = func() {
log.SetOutput(ioutil.Discard)
}
opts.LogFile = func(logfile string) {
f, err := os.OpenFile(logfile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
log.Fatalf("cannot write to file %s: %v", logfile, err)
}
log.SetOutput(f)
}
if _, err := parser.Parse(); err != nil {
os.Exit(1)
}
}