[bugfix] Fix Swagger spec and add test script (#2698)

* Add Swagger spec test script

* Fix Swagger spec errors not related to statuses with polls

* Add API tests that post a status with a poll

* Fix creating a status with a poll from form params

* Fix Swagger spec errors related to statuses with polls (this is the last error)

* Fix Swagger spec warnings not related to unused definitions

* Suppress a duplicate list update params definition that was somehow causing wrong param names

* Add Swagger test to CI

- updates Drone config
- vendorizes go-swagger
- fixes a file extension issue that caused the test script to generate JSON instead of YAML with the vendorized version

* Put `Sample: ` on its own line everywhere

* Remove unused id param from emojiCategoriesGet

* Add 5 more pairs of profile fields to account update API Swagger

* Remove Swagger prefix from dummy fields

It makes the generated code look weird

* Manually annotate params for statusCreate operation

* Fix all remaining Swagger spec warnings

- Change some models into operation parameters
- Ignore models that already correspond to manually documented operation parameters but can't be trivially changed (those with file fields)

* Documented that creating a status with scheduled_at isn't implemented yet

* sign drone.yml

* Fix filter API Swagger errors

* fixup! Fix filter API Swagger errors

---------

Co-authored-by: tobi <tobi.smethurst@protonmail.com>
This commit is contained in:
Vyr Cossont 2024-03-06 09:05:45 -08:00 committed by GitHub
commit fc3741365c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
672 changed files with 135624 additions and 713 deletions

View file

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

View file

@ -0,0 +1,145 @@
package commands
import (
"encoding/json"
"fmt"
"io"
"log"
"os"
"errors"
"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"`
Args struct {
OldSpec string `positional-arg-name:"{old spec}"`
NewSpec string `positional-arg-name:"{new spec}"`
} `required:"2" positional-args:"specs" description:"Input specs to be diff-ed"`
}
// Execute diffs the two specs provided
func (c *DiffCommand) Execute(_ []string) error {
if c.Args.OldSpec == "" || c.Args.NewSpec == "" {
return errors.New(`missing arguments for diff command (use --help for more info)`)
}
c.printInfo()
var (
output io.WriteCloser
err error
)
if c.Destination != "stdout" {
output, err = os.OpenFile(c.Destination, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
if err != nil {
return fmt.Errorf("%s: %w", c.Destination, err)
}
defer func() {
_ = output.Close()
}()
} else {
output = os.Stdout
}
diffs, err := c.getDiffs()
if err != nil {
return err
}
ignores, err := c.readIgnores()
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())
}
}
var (
input io.Reader
warn error
)
if c.Format != JSONFormat && c.OnlyBreakingChanges {
input, err, warn = diffs.ReportCompatibility()
} else {
input, err, warn = diffs.ReportAllDiffs(c.Format == JSONFormat)
}
if err != nil {
return err
}
_, err = io.Copy(output, input)
if err != nil {
return err
}
return warn
}
func (c *DiffCommand) readIgnores() (diff.SpecDifferences, error) {
ignoreFile := c.IgnoreFile
ignoreDiffs := diff.SpecDifferences{}
if ignoreFile == "none specified" || ignoreFile == "" {
return ignoreDiffs, nil
}
// Open our jsonFile
jsonFile, err := os.Open(ignoreFile)
if err != nil {
return nil, fmt.Errorf("%s: %w", ignoreFile, err)
}
defer func() {
_ = jsonFile.Close()
}()
byteValue, err := io.ReadAll(jsonFile)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", ignoreFile, err)
}
err = json.Unmarshal(byteValue, &ignoreDiffs)
if err != nil {
return nil, err
}
return ignoreDiffs, nil
}
func (c *DiffCommand) getDiffs() (diff.SpecDifferences, error) {
oldSpecPath, newSpecPath := c.Args.OldSpec, c.Args.NewSpec
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())
}
func (c *DiffCommand) printInfo() {
log.Println("Run Config:")
log.Printf("Spec1: %s", c.Args.OldSpec)
log.Printf("Spec2: %s", c.Args.NewSpec)
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)
}

View file

@ -0,0 +1,106 @@
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 diff expression
func (f fromArrayStruct) DiffsTo(toArray []string) (added, deleted, common []string) {
inFrom := 1
inTo := 2
if f.from == nil {
return toArray, []string{}, []string{}
}
m := make(map[string]int, len(toArray))
added = make([]string, 0, len(toArray))
deleted = make([]string, 0, len(f.from))
common = make([]string, 0, len(f.from))
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,266 @@
package diff
import (
"fmt"
"strings"
"github.com/go-openapi/spec"
)
// CompareEnums returns added, deleted enum values
func 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
}
// CompareProperties recursive property comparison
func CompareProperties(location DifferenceLocation, schema1 *spec.Schema, schema2 *spec.Schema, getRefFn1 SchemaFromRefFn, getRefFn2 SchemaFromRefFn, cmp CompareSchemaFn) []SpecDifference {
propDiffs := []SpecDifference{}
if schema1.Properties == nil && schema2.Properties == nil {
return propDiffs
}
schema1Props := propertiesFor(schema1, getRefFn1)
schema2Props := propertiesFor(schema2, getRefFn2)
// find deleted and changed properties
for eachProp1Name, eachProp1 := range schema1Props {
eachProp1 := eachProp1
childLoc := addChildDiffNode(location, eachProp1Name, eachProp1.Schema)
if eachProp2, ok := schema2Props[eachProp1Name]; ok {
diffs := CheckToFromRequired(eachProp1.Required, eachProp2.Required)
if len(diffs) > 0 {
for _, diff := range diffs {
propDiffs = append(propDiffs, SpecDifference{DifferenceLocation: childLoc, Code: diff.Change})
}
}
cmp(childLoc, eachProp1.Schema, eachProp2.Schema)
} else {
propDiffs = append(propDiffs, SpecDifference{DifferenceLocation: childLoc, Code: DeletedProperty})
}
}
// find added properties
for eachProp2Name, eachProp2 := range schema2.Properties {
eachProp2 := eachProp2
if _, ok := schema1.Properties[eachProp2Name]; !ok {
childLoc := addChildDiffNode(location, eachProp2Name, &eachProp2)
propDiffs = append(propDiffs, SpecDifference{DifferenceLocation: childLoc, Code: AddedProperty})
}
}
return propDiffs
}
// CompareFloatValues compares a float data item
func CompareFloatValues(fieldName string, val1 *float64, val2 *float64, ifGreaterCode SpecChangeCode, ifLessCode SpecChangeCode) []TypeDiff {
diffs := []TypeDiff{}
if val1 != nil && val2 != nil {
if *val2 > *val1 {
diffs = append(diffs, TypeDiff{Change: ifGreaterCode, Description: fmt.Sprintf("%s %f->%f", fieldName, *val1, *val2)})
} else if *val2 < *val1 {
diffs = append(diffs, TypeDiff{Change: ifLessCode, Description: fmt.Sprintf("%s %f->%f", fieldName, *val1, *val2)})
}
} else {
if val1 != val2 {
if val1 != nil {
diffs = append(diffs, TypeDiff{Change: DeletedConstraint, Description: fmt.Sprintf("%s(%f)", fieldName, *val1)})
} else {
diffs = append(diffs, TypeDiff{Change: AddedConstraint, Description: fmt.Sprintf("%s(%f)", fieldName, *val2)})
}
}
}
return diffs
}
// CompareIntValues compares to int data items
func CompareIntValues(fieldName string, val1 *int64, val2 *int64, ifGreaterCode SpecChangeCode, ifLessCode SpecChangeCode) []TypeDiff {
diffs := []TypeDiff{}
if val1 != nil && val2 != nil {
if *val2 > *val1 {
diffs = append(diffs, TypeDiff{Change: ifGreaterCode, Description: fmt.Sprintf("%s %d->%d", fieldName, *val1, *val2)})
} else if *val2 < *val1 {
diffs = append(diffs, TypeDiff{Change: ifLessCode, Description: fmt.Sprintf("%s %d->%d", fieldName, *val1, *val2)})
}
} else {
if val1 != val2 {
if val1 != nil {
diffs = append(diffs, TypeDiff{Change: DeletedConstraint, Description: fmt.Sprintf("%s(%d)", fieldName, *val1)})
} else {
diffs = append(diffs, TypeDiff{Change: AddedConstraint, Description: fmt.Sprintf("%s(%d)", fieldName, *val2)})
}
}
}
return diffs
}
// CheckToFromPrimitiveType check for diff to or from a primitive
func CheckToFromPrimitiveType(diffs []TypeDiff, type1, type2 interface{}) []TypeDiff {
type1IsPrimitive := isPrimitive(type1)
type2IsPrimitive := isPrimitive(type2)
// Primitive to Obj or Obj to Primitive
if type1IsPrimitive != type2IsPrimitive {
typeStr1, isarray1 := getSchemaType(type1)
typeStr2, isarray2 := getSchemaType(type2)
return addTypeDiff(diffs, TypeDiff{Change: ChangedType, FromType: formatTypeString(typeStr1, isarray1), ToType: formatTypeString(typeStr2, isarray2)})
}
return diffs
}
// CheckRefChange has the property ref changed
func CheckRefChange(diffs []TypeDiff, type1, type2 interface{}) (diffReturn []TypeDiff) {
diffReturn = diffs
if isRefType(type1) && isRefType(type2) {
// both refs but to different objects (TODO detect renamed object)
ref1 := definitionFromRef(getRef(type1))
ref2 := definitionFromRef(getRef(type2))
if ref1 != ref2 {
diffReturn = addTypeDiff(diffReturn, TypeDiff{Change: RefTargetChanged, FromType: getSchemaTypeStr(type1), ToType: getSchemaTypeStr(type2)})
}
} else if isRefType(type1) != isRefType(type2) {
diffReturn = addTypeDiff(diffReturn, TypeDiff{Change: ChangedType, FromType: getSchemaTypeStr(type1), ToType: getSchemaTypeStr(type2)})
}
return
}
// checkNumericTypeChanges checks for changes to or from a numeric type
func checkNumericTypeChanges(diffs []TypeDiff, type1, type2 *spec.SchemaProps) []TypeDiff {
// Number
_, type1IsNumeric := numberWideness[type1.Type[0]]
_, type2IsNumeric := numberWideness[type2.Type[0]]
if type1IsNumeric && type2IsNumeric {
foundDiff := false
if type1.ExclusiveMaximum && !type2.ExclusiveMaximum {
diffs = addTypeDiff(diffs, TypeDiff{Change: WidenedType, Description: fmt.Sprintf("Exclusive Maximum Removed:%v->%v", type1.ExclusiveMaximum, type2.ExclusiveMaximum)})
foundDiff = true
}
if !type1.ExclusiveMaximum && type2.ExclusiveMaximum {
diffs = addTypeDiff(diffs, TypeDiff{Change: NarrowedType, Description: fmt.Sprintf("Exclusive Maximum Added:%v->%v", type1.ExclusiveMaximum, type2.ExclusiveMaximum)})
foundDiff = true
}
if type1.ExclusiveMinimum && !type2.ExclusiveMinimum {
diffs = addTypeDiff(diffs, TypeDiff{Change: WidenedType, Description: fmt.Sprintf("Exclusive Minimum Removed:%v->%v", type1.ExclusiveMaximum, type2.ExclusiveMaximum)})
foundDiff = true
}
if !type1.ExclusiveMinimum && type2.ExclusiveMinimum {
diffs = addTypeDiff(diffs, TypeDiff{Change: NarrowedType, Description: fmt.Sprintf("Exclusive Minimum Added:%v->%v", type1.ExclusiveMinimum, type2.ExclusiveMinimum)})
foundDiff = true
}
if !foundDiff {
maxDiffs := CompareFloatValues("Maximum", type1.Maximum, type2.Maximum, WidenedType, NarrowedType)
diffs = append(diffs, maxDiffs...)
minDiffs := CompareFloatValues("Minimum", type1.Minimum, type2.Minimum, NarrowedType, WidenedType)
diffs = append(diffs, minDiffs...)
}
}
return diffs
}
// CheckStringTypeChanges checks for changes to or from a string type
func CheckStringTypeChanges(diffs []TypeDiff, type1, type2 *spec.SchemaProps) []TypeDiff {
// string changes
if type1.Type[0] == StringType &&
type2.Type[0] == StringType {
minLengthDiffs := CompareIntValues("MinLength", type1.MinLength, type2.MinLength, NarrowedType, WidenedType)
diffs = append(diffs, minLengthDiffs...)
maxLengthDiffs := CompareIntValues("MaxLength", type1.MinLength, type2.MinLength, WidenedType, NarrowedType)
diffs = append(diffs, maxLengthDiffs...)
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 := CompareEnums(type1.Enum, type2.Enum)
diffs = append(diffs, enumDiffs...)
}
}
}
return diffs
}
// CheckToFromRequired checks for changes to or from a required property
func CheckToFromRequired(required1, required2 bool) (diffs []TypeDiff) {
if required1 != required2 {
code := ChangedOptionalToRequired
if required1 {
code = ChangedRequiredToOptional
}
diffs = addTypeDiff(diffs, TypeDiff{Change: code})
}
return diffs
}
const objType = "object"
func getTypeHierarchyChange(type1, type2 string) TypeDiff {
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 isRefType(item interface{}) bool {
switch s := item.(type) {
case spec.Refable:
return s.Ref.String() != ""
case *spec.Schema:
return s.Ref.String() != ""
case *spec.SchemaProps:
return s.Ref.String() != ""
case *spec.SimpleSchema:
return false
default:
return false
}
}

View file

@ -0,0 +1,111 @@
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,
DeletedConstraint: Breaking,
AddedConstraint: NonBreaking,
DeletedExtension: Warning,
AddedExtension: Warning,
},
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,
ChangedOptionalToRequired: Breaking,
ChangedRequiredToOptional: NonBreaking,
AddedEnumValue: NonBreaking,
DeletedEnumValue: Breaking,
ChangedDescripton: NonBreaking,
AddedDescripton: NonBreaking,
DeletedDescripton: NonBreaking,
ChangedTag: NonBreaking,
AddedTag: NonBreaking,
DeletedTag: NonBreaking,
DeletedConstraint: NonBreaking,
AddedConstraint: Breaking,
ChangedDefault: Warning,
AddedDefault: Warning,
DeletedDefault: Warning,
ChangedExample: NonBreaking,
AddedExample: NonBreaking,
DeletedExample: NonBreaking,
ChangedCollectionFormat: Breaking,
DeletedExtension: Warning,
AddedExtension: Warning,
},
ForChange: map[SpecChangeCode]Compatibility{
NoChangeDetected: NonBreaking,
AddedEndpoint: NonBreaking,
DeletedEndpoint: Breaking,
DeletedDeprecatedEndpoint: NonBreaking,
AddedConsumesFormat: NonBreaking,
DeletedConsumesFormat: Breaking,
AddedProducesFormat: NonBreaking,
DeletedProducesFormat: Breaking,
AddedSchemes: NonBreaking,
DeletedSchemes: Breaking,
ChangedHostURL: Breaking,
ChangedBasePath: Breaking,
ChangedDescripton: NonBreaking,
AddedDescripton: NonBreaking,
DeletedDescripton: NonBreaking,
ChangedTag: NonBreaking,
AddedTag: NonBreaking,
DeletedTag: NonBreaking,
RefTargetChanged: Breaking,
RefTargetRenamed: NonBreaking,
AddedDefinition: NonBreaking,
DeletedDefinition: NonBreaking,
DeletedExtension: Warning,
AddedExtension: Warning,
},
}
}
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,337 @@
package diff
import (
"bytes"
"encoding/json"
"fmt"
"log"
)
// 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
// 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
// ChangedOptionalToRequired - An optional parameter is now required in the new spec
ChangedOptionalToRequired
// ChangedRequiredToOptional - An required parameter is now optional in the new spec
ChangedRequiredToOptional
// 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
// RefTargetChanged Changed a ref to point to a different object
RefTargetChanged
// RefTargetRenamed Renamed a ref to point to the same object
RefTargetRenamed
// DeletedConstraint Deleted a schema constraint
DeletedConstraint
// AddedConstraint Added a schema constraint
AddedConstraint
// DeletedDefinition removed one of the definitions
DeletedDefinition
// AddedDefinition removed one of the definitions
AddedDefinition
// ChangedDefault - Changed default value
ChangedDefault
// AddedDefault - Added a default value
AddedDefault
// DeletedDefault - Deleted a default value
DeletedDefault
// ChangedExample - Changed an example value
ChangedExample
// AddedExample - Added an example value
AddedExample
// DeletedExample - Deleted an example value
DeletedExample
// ChangedCollectionFormat - A collectionFormat has been changed to a collectionFormat whose relative compatibility cannot be determined
ChangedCollectionFormat
// DeletedExtension deleted an extension
DeletedExtension
// AddedExtension added an extension
AddedExtension
)
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",
ChangedOptionalToRequired: "Changed optional param to required",
ChangedRequiredToOptional: "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",
RefTargetChanged: "Changed ref to different object",
RefTargetRenamed: "Changed ref to renamed object",
DeletedConstraint: "Deleted a schema constraint",
AddedConstraint: "Added a schema constraint",
DeletedDefinition: "Deleted a schema definition",
AddedDefinition: "Added a schema definition",
ChangedDefault: "Default value is changed",
AddedDefault: "Default value is added",
DeletedDefault: "Default value is removed",
ChangedExample: "Example value is changed",
AddedExample: "Example value is added",
DeletedExample: "Example value is removed",
ChangedCollectionFormat: "Changed collection format",
DeletedExtension: "Deleted Extension",
AddedExtension: "Added Extension",
}
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",
ChangedOptionalToRequired: "ChangedOptionalToRequiredParam",
ChangedRequiredToOptional: "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",
RefTargetChanged: "RefTargetChanged",
RefTargetRenamed: "RefTargetRenamed",
DeletedConstraint: "DeletedConstraint",
AddedConstraint: "AddedConstraint",
DeletedDefinition: "DeletedDefinition",
AddedDefinition: "AddedDefinition",
ChangedDefault: "ChangedDefault",
AddedDefault: "AddedDefault",
DeletedDefault: "DeletedDefault",
ChangedExample: "ChangedExample",
AddedExample: "AddedExample",
DeletedExample: "DeletedExample",
ChangedCollectionFormat: "ChangedCollectionFormat",
DeletedExtension: "DeletedExtension",
AddedExtension: "AddedExtension",
}
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 {
log.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
// Warning changes are technically non-breaking but can cause behavior changes in client and thus should be reported differently
Warning
)
func (s Compatibility) String() string {
return toStringCompatibility[s]
}
var toStringCompatibility = map[Compatibility]string{
Breaking: "Breaking",
NonBreaking: "NonBreaking",
Warning: "Warning",
}
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,82 @@
package diff
import (
"fmt"
"github.com/go-openapi/spec"
)
// 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 = fmt.Sprintf("%s<array[%s]>", name, n.TypeName)
} else if len(n.TypeName) > 0 {
name = fmt.Sprintf("%s<%s>", name, n.TypeName)
}
if n.ChildNode != nil {
return fmt.Sprintf("%s.%s", name, n.ChildNode.String())
}
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 {
newChild := n.ChildNode
if newChild != nil {
newChild = newChild.Copy()
}
newNode := Node{
Field: n.Field,
TypeName: n.TypeName,
IsArray: n.IsArray,
ChildNode: newChild,
}
return &newNode
}
func getSchemaDiffNode(name string, schema interface{}) *Node {
node := Node{
Field: name,
}
if schema != nil {
switch s := schema.(type) {
case spec.Refable:
node.TypeName, node.IsArray = getSchemaType(s)
case *spec.Schema:
node.TypeName, node.IsArray = getSchemaType(s.SchemaProps)
case spec.SimpleSchema:
node.TypeName, node.IsArray = getSchemaType(s)
case *spec.SimpleSchema:
node.TypeName, node.IsArray = getSchemaType(s)
case *spec.SchemaProps:
node.TypeName, node.IsArray = getSchemaType(s)
case spec.SchemaProps:
node.TypeName, node.IsArray = getSchemaType(&s)
default:
node.TypeName = fmt.Sprintf("Unknown type %v", schema)
}
}
return &node
}

View file

@ -0,0 +1,118 @@
package diff
import (
"bytes"
"encoding/json"
"fmt"
"io"
"github.com/go-openapi/spec"
)
// ArrayType const for array
var ArrayType = "array"
// ObjectType const for object
var ObjectType = "object"
// 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"`
Extensions spec.Extensions `json:"extensions"`
}
// 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 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) (io.ReadWriter, error) {
var out bytes.Buffer
err := json.Indent(&out, b, "", " ")
return &out, 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,126 @@
package diff
import (
"fmt"
"strings"
"github.com/go-openapi/spec"
)
func getTypeFromSchema(schema *spec.Schema) (typeName string, isArray bool) {
refStr := definitionFromRef(schema.Ref)
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 getTypeFromSimpleSchema(schema *spec.SimpleSchema) (typeName string, isArray bool) {
typeName = schema.Type
format := schema.Format
if len(format) > 0 {
typeName = fmt.Sprintf("%s.%s", typeName, format)
}
if typeName == ArrayType {
typeName, _ = getSchemaType(&schema.Items.SimpleSchema)
return typeName, true
}
return typeName, false
}
func getTypeFromSchemaProps(schema *spec.SchemaProps) (typeName string, isArray bool) {
refStr := definitionFromRef(schema.Ref)
if len(refStr) > 0 {
return refStr, false
}
if len(schema.Type) > 0 {
typeName = schema.Type[0]
format := schema.Format
if len(format) > 0 {
typeName = fmt.Sprintf("%s.%s", typeName, format)
}
if typeName == ArrayType {
typeName, _ = getSchemaType(&schema.Items.Schema.SchemaProps)
return typeName, true
}
}
return typeName, false
}
func getSchemaTypeStr(item interface{}) string {
typeStr, isArray := getSchemaType(item)
return formatTypeString(typeStr, isArray)
}
func getSchemaType(item interface{}) (typeName string, isArray bool) {
switch s := item.(type) {
case *spec.Schema:
typeName, isArray = getTypeFromSchema(s)
case *spec.SchemaProps:
typeName, isArray = getTypeFromSchemaProps(s)
case spec.SchemaProps:
typeName, isArray = getTypeFromSchemaProps(&s)
case spec.SimpleSchema:
typeName, isArray = getTypeFromSimpleSchema(&s)
case *spec.SimpleSchema:
typeName, isArray = getTypeFromSimpleSchema(s)
default:
typeName = "unknown"
}
return
}
func formatTypeString(typ string, isarray bool) string {
if isarray {
return fmt.Sprintf("<array[%s]>", typ)
}
return fmt.Sprintf("<%s>", typ)
}
func definitionFromRef(ref spec.Ref) string {
url := ref.GetURL()
if url == nil {
return ""
}
fragmentParts := strings.Split(url.Fragment, "/")
numParts := len(fragmentParts)
return fragmentParts[numParts-1]
}
func isArray(item interface{}) bool {
switch s := item.(type) {
case *spec.Schema:
return isArrayType(s.Type)
case *spec.SchemaProps:
return isArrayType(s.Type)
case *spec.SimpleSchema:
return isArrayType(spec.StringOrArray{s.Type})
default:
return false
}
}
func isPrimitive(item interface{}) bool {
switch s := item.(type) {
case *spec.Schema:
return isPrimitiveType(s.Type)
case *spec.SchemaProps:
return isPrimitiveType(s.Type)
case spec.StringOrArray:
return isPrimitiveType(s)
default:
return false
}
}

View file

@ -0,0 +1,759 @@
package diff
import (
"fmt"
"strings"
"github.com/go-openapi/spec"
)
// StringType For identifying string types
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
Info1 *spec.Info
Info2 *spec.Info
ReferencedDefinitions map[string]bool
schemasCompared map[string]struct{}
}
// NewSpecAnalyser returns an empty SpecDiffs
func NewSpecAnalyser() *SpecAnalyser {
return &SpecAnalyser{
Diffs: SpecDifferences{},
ReferencedDefinitions: map[string]bool{},
}
}
// Analyse the differences in two specs
func (sd *SpecAnalyser) Analyse(spec1, spec2 *spec.Swagger) error {
sd.schemasCompared = make(map[string]struct{})
sd.Definitions1 = spec1.Definitions
sd.Definitions2 = spec2.Definitions
sd.Info1 = spec1.Info
sd.Info2 = spec2.Info
sd.urlMethods1 = getURLMethodsFor(spec1)
sd.urlMethods2 = getURLMethodsFor(spec2)
sd.analyseSpecMetadata(spec1, spec2)
sd.analyseEndpoints()
sd.analyseRequestParams()
sd.analyseEndpointData()
sd.analyseResponseParams()
sd.analyseExtensions(spec1, spec2)
sd.AnalyseDefinitions()
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()
}
// AnalyseDefinitions check for changes to definition objects not referenced in any endpoint
func (sd *SpecAnalyser) AnalyseDefinitions() {
alreadyReferenced := map[string]bool{}
for k := range sd.ReferencedDefinitions {
alreadyReferenced[k] = true
}
location := DifferenceLocation{Node: &Node{Field: "Spec Definitions"}}
for name1, sch := range sd.Definitions1 {
schema1 := sch
if _, ok := alreadyReferenced[name1]; !ok {
childLocation := location.AddNode(&Node{Field: name1})
if schema2, ok := sd.Definitions2[name1]; ok {
sd.compareSchema(childLocation, &schema1, &schema2)
} else {
sd.addDiffs(childLocation, []TypeDiff{{Change: DeletedDefinition}})
}
}
}
for name2 := range sd.Definitions2 {
if _, ok := sd.Definitions1[name2]; !ok {
childLocation := location.AddNode(&Node{Field: name2})
sd.addDiffs(childLocation, []TypeDiff{{Change: AddedDefinition}})
}
}
}
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: fmt.Sprintf(`"%s"`, eachAddedTag)})
}
for _, eachDeletedTag := range deletedTags {
sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: location, Code: DeletedTag, DiffInfo: fmt.Sprintf(`"%s"`, eachDeletedTag)})
}
sd.compareDescripton(location, op1.Operation.Description, op2.Operation.Description)
}
}
}
func (sd *SpecAnalyser) analyseRequestParams() {
locations := []string{"query", "path", "body", "header", "formData"}
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.SimpleSchema))
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.SimpleSchema))
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 eachURLMethodFrom2, op2 := range sd.urlMethods2 {
// present in both specs? Use key from spec 2 to lookup in spec 1
if op1, ok := sd.urlMethods1[eachURLMethodFrom2]; 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: eachURLMethodFrom2.Path, Method: eachURLMethodFrom2.Method, Response: code1, Node: getSchemaDiffNode("Body", op1Responses[code1].Schema)}
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")
// Iterate Spec2 Headers looking for added and updated
location := DifferenceLocation{URL: eachURLMethodFrom2.Path, Method: eachURLMethodFrom2.Method, Response: code2, Node: headerRootNode}
for op2HeaderName, op2Header := range op2Response.ResponseProps.Headers {
if op1Header, ok := op1Headers[op2HeaderName]; ok {
diffs := sd.CompareProps(forHeader(op1Header), forHeader(op2Header))
sd.addDiffs(location, diffs)
} else {
sd.Diffs = sd.Diffs.addDiff(SpecDifference{
DifferenceLocation: location.AddNode(getSchemaDiffNode(op2HeaderName, &op2Header.SimpleSchema)),
Code: AddedResponseHeader})
}
}
for op1HeaderName := range op1Response.ResponseProps.Headers {
if _, ok := op2Response.ResponseProps.Headers[op1HeaderName]; !ok {
op1Header := op1Response.ResponseProps.Headers[op1HeaderName]
sd.Diffs = sd.Diffs.addDiff(SpecDifference{
DifferenceLocation: location.AddNode(getSchemaDiffNode(op1HeaderName, &op1Header.SimpleSchema)),
Code: DeletedResponseHeader})
}
}
schem := op1Response.Schema
node := getNameOnlyDiffNode("NoContent")
if schem != nil {
node = getSchemaDiffNode("Body", &schem.SchemaProps)
}
responseLocation := DifferenceLocation{URL: eachURLMethodFrom2.Path,
Method: eachURLMethodFrom2.Method,
Response: code2,
Node: node}
sd.compareDescripton(responseLocation, op1Response.Description, op2Response.Description)
if op1Response.Schema != nil {
sd.compareSchema(
DifferenceLocation{URL: eachURLMethodFrom2.Path, Method: eachURLMethodFrom2.Method, Response: code2, Node: getSchemaDiffNode("Body", op1Response.Schema)},
op1Response.Schema,
op2Response.Schema)
}
} else {
// op2Response
sd.Diffs = sd.Diffs.addDiff(SpecDifference{
DifferenceLocation: DifferenceLocation{URL: eachURLMethodFrom2.Path, Method: eachURLMethodFrom2.Method, Response: code2, Node: getSchemaDiffNode("Body", op2Response.Schema)},
Code: AddedResponse})
}
}
}
}
}
func (sd *SpecAnalyser) analyseExtensions(spec1, spec2 *spec.Swagger) {
// root
specLoc := DifferenceLocation{Node: &Node{Field: "Spec"}}
sd.checkAddedExtensions(spec1.Extensions, spec2.Extensions, specLoc, "")
sd.checkDeletedExtensions(spec1.Extensions, spec2.Extensions, specLoc, "")
sd.analyzeInfoExtensions()
sd.analyzeTagExtensions(spec1, spec2)
sd.analyzeSecurityDefinitionExtensions(spec1, spec2)
sd.analyzeOperationExtensions()
}
func (sd *SpecAnalyser) analyzeOperationExtensions() {
for urlMethod, op2 := range sd.urlMethods2 {
pathAndMethodLoc := DifferenceLocation{URL: urlMethod.Path, Method: urlMethod.Method}
if op1, ok := sd.urlMethods1[urlMethod]; ok {
sd.checkAddedExtensions(op1.Extensions, op2.Extensions, DifferenceLocation{URL: urlMethod.Path}, "")
sd.checkAddedExtensions(op1.Operation.Responses.Extensions, op2.Operation.Responses.Extensions, pathAndMethodLoc, "Responses")
sd.checkAddedExtensions(op1.Operation.Extensions, op2.Operation.Extensions, pathAndMethodLoc, "")
for code, resp := range op1.Operation.Responses.StatusCodeResponses {
for hdr, h := range resp.Headers {
op2StatusCode, ok := op2.Operation.Responses.StatusCodeResponses[code]
if ok {
if _, ok = op2StatusCode.Headers[hdr]; ok {
sd.checkAddedExtensions(h.Extensions, op2StatusCode.Headers[hdr].Extensions, DifferenceLocation{URL: urlMethod.Path, Method: urlMethod.Method, Node: getNameOnlyDiffNode("Headers")}, hdr)
}
}
}
resp2 := op2.Operation.Responses.StatusCodeResponses[code]
sd.analyzeSchemaExtensions(resp.Schema, resp2.Schema, code, urlMethod)
}
}
}
for urlMethod, op1 := range sd.urlMethods1 {
pathAndMethodLoc := DifferenceLocation{URL: urlMethod.Path, Method: urlMethod.Method}
if op2, ok := sd.urlMethods2[urlMethod]; ok {
sd.checkDeletedExtensions(op1.Extensions, op2.Extensions, DifferenceLocation{URL: urlMethod.Path}, "")
sd.checkDeletedExtensions(op1.Operation.Responses.Extensions, op2.Operation.Responses.Extensions, pathAndMethodLoc, "Responses")
sd.checkDeletedExtensions(op1.Operation.Extensions, op2.Operation.Extensions, pathAndMethodLoc, "")
for code, resp := range op1.Operation.Responses.StatusCodeResponses {
for hdr, h := range resp.Headers {
op2StatusCode, ok := op2.Operation.Responses.StatusCodeResponses[code]
if ok {
if _, ok = op2StatusCode.Headers[hdr]; ok {
sd.checkDeletedExtensions(h.Extensions, op2StatusCode.Headers[hdr].Extensions, DifferenceLocation{URL: urlMethod.Path, Method: urlMethod.Method, Node: getNameOnlyDiffNode("Headers")}, hdr)
}
}
}
}
}
}
}
func (sd *SpecAnalyser) analyzeSecurityDefinitionExtensions(spec1 *spec.Swagger, spec2 *spec.Swagger) {
securityDefLoc := DifferenceLocation{Node: &Node{Field: "Security Definitions"}}
for key, securityDef := range spec1.SecurityDefinitions {
if securityDef2, ok := spec2.SecurityDefinitions[key]; ok {
sd.checkAddedExtensions(securityDef.Extensions, securityDef2.Extensions, securityDefLoc, "")
}
}
for key, securityDef := range spec2.SecurityDefinitions {
if securityDef1, ok := spec1.SecurityDefinitions[key]; ok {
sd.checkDeletedExtensions(securityDef1.Extensions, securityDef.Extensions, securityDefLoc, "")
}
}
}
func (sd *SpecAnalyser) analyzeSchemaExtensions(schema1, schema2 *spec.Schema, code int, urlMethod URLMethod) {
if schema1 != nil && schema2 != nil {
diffLoc := DifferenceLocation{Response: code, URL: urlMethod.Path, Method: urlMethod.Method, Node: getSchemaDiffNode("Body", schema2)}
sd.checkAddedExtensions(schema1.Extensions, schema2.Extensions, diffLoc, "")
sd.checkDeletedExtensions(schema1.Extensions, schema2.Extensions, diffLoc, "")
if schema1.Items != nil && schema2.Items != nil {
sd.analyzeSchemaExtensions(schema1.Items.Schema, schema2.Items.Schema, code, urlMethod)
for i := range schema1.Items.Schemas {
s1 := schema1.Items.Schemas[i]
for j := range schema2.Items.Schemas {
s2 := schema2.Items.Schemas[j]
sd.analyzeSchemaExtensions(&s1, &s2, code, urlMethod)
}
}
}
}
}
func (sd *SpecAnalyser) analyzeInfoExtensions() {
if sd.Info1 != nil && sd.Info2 != nil {
diffLocation := DifferenceLocation{Node: &Node{Field: "Spec Info"}}
sd.checkAddedExtensions(sd.Info1.Extensions, sd.Info2.Extensions, diffLocation, "")
sd.checkDeletedExtensions(sd.Info1.Extensions, sd.Info2.Extensions, diffLocation, "")
if sd.Info1.Contact != nil && sd.Info2.Contact != nil {
diffLocation = DifferenceLocation{Node: &Node{Field: "Spec Info.Contact"}}
sd.checkAddedExtensions(sd.Info1.Contact.Extensions, sd.Info2.Contact.Extensions, diffLocation, "")
sd.checkDeletedExtensions(sd.Info1.Contact.Extensions, sd.Info2.Contact.Extensions, diffLocation, "")
}
if sd.Info1.License != nil && sd.Info2.License != nil {
diffLocation = DifferenceLocation{Node: &Node{Field: "Spec Info.License"}}
sd.checkAddedExtensions(sd.Info1.License.Extensions, sd.Info2.License.Extensions, diffLocation, "")
sd.checkDeletedExtensions(sd.Info1.License.Extensions, sd.Info2.License.Extensions, diffLocation, "")
}
}
}
func (sd *SpecAnalyser) analyzeTagExtensions(spec1 *spec.Swagger, spec2 *spec.Swagger) {
diffLocation := DifferenceLocation{Node: &Node{Field: "Spec Tags"}}
for _, spec2Tag := range spec2.Tags {
for _, spec1Tag := range spec1.Tags {
if spec2Tag.Name == spec1Tag.Name {
sd.checkAddedExtensions(spec1Tag.Extensions, spec2Tag.Extensions, diffLocation, "")
}
}
}
for _, spec1Tag := range spec1.Tags {
for _, spec2Tag := range spec2.Tags {
if spec1Tag.Name == spec2Tag.Name {
sd.checkDeletedExtensions(spec1Tag.Extensions, spec2Tag.Extensions, diffLocation, "")
}
}
}
}
func (sd *SpecAnalyser) checkAddedExtensions(extensions1 spec.Extensions, extensions2 spec.Extensions, diffLocation DifferenceLocation, fieldPrefix string) {
for extKey := range extensions2 {
if _, ok := extensions1[extKey]; !ok {
if fieldPrefix != "" {
extKey = fmt.Sprintf("%s.%s", fieldPrefix, extKey)
}
sd.Diffs = sd.Diffs.addDiff(SpecDifference{
DifferenceLocation: diffLocation.AddNode(&Node{Field: extKey}),
Code: AddedExtension,
Compatibility: Warning, // this could potentially be a breaking change
})
}
}
}
func (sd *SpecAnalyser) checkDeletedExtensions(extensions1 spec.Extensions, extensions2 spec.Extensions, diffLocation DifferenceLocation, fieldPrefix string) {
for extKey := range extensions1 {
if _, ok := extensions2[extKey]; !ok {
if fieldPrefix != "" {
extKey = fmt.Sprintf("%s.%s", fieldPrefix, extKey)
}
sd.Diffs = sd.Diffs.addDiff(SpecDifference{
DifferenceLocation: diffLocation.AddNode(&Node{Field: extKey}),
Code: DeletedExtension,
Compatibility: Warning, // this could potentially be a breaking change
})
}
}
}
func addTypeDiff(diffs []TypeDiff, diff TypeDiff) []TypeDiff {
if diff.Change != NoChangeDetected {
diffs = append(diffs, diff)
}
return diffs
}
// CompareProps computes type specific property diffs
func (sd *SpecAnalyser) CompareProps(type1, type2 *spec.SchemaProps) []TypeDiff {
diffs := []TypeDiff{}
diffs = CheckToFromPrimitiveType(diffs, type1, type2)
if len(diffs) > 0 {
return diffs
}
if isArray(type1) {
maxItemDiffs := CompareIntValues("MaxItems", type1.MaxItems, type2.MaxItems, WidenedType, NarrowedType)
diffs = append(diffs, maxItemDiffs...)
minItemsDiff := CompareIntValues("MinItems", type1.MinItems, type2.MinItems, NarrowedType, WidenedType)
diffs = append(diffs, minItemsDiff...)
}
if len(diffs) > 0 {
return diffs
}
diffs = CheckRefChange(diffs, type1, type2)
if len(diffs) > 0 {
return diffs
}
if !(isPrimitiveType(type1.Type) && isPrimitiveType(type2.Type)) {
return diffs
}
// check primitive type hierarchy change eg string -> integer = NarrowedChange
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 = CheckStringTypeChanges(diffs, type1, type2)
if len(diffs) > 0 {
return diffs
}
diffs = 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 {
if len(name) > 0 {
childLocation = childLocation.AddNode(getSchemaDiffNode(name, param2.Schema))
}
sd.compareSchema(childLocation, param1.Schema, param2.Schema)
}
diffs := sd.CompareProps(forParam(param1), forParam(param2))
childLocation = childLocation.AddNode(getSchemaDiffNode(name, &param2.SimpleSchema))
if len(diffs) > 0 {
sd.addDiffs(childLocation, diffs)
}
diffs = CheckToFromRequired(param1.Required, param2.Required)
if len(diffs) > 0 {
sd.addDiffs(childLocation, diffs)
}
sd.compareSimpleSchema(childLocation, &param1.SimpleSchema, &param2.SimpleSchema)
}
func (sd *SpecAnalyser) addTypeDiff(location DifferenceLocation, diff *TypeDiff) {
diffCopy := diff
desc := diffCopy.Description
if len(desc) == 0 {
if diffCopy.FromType != diffCopy.ToType {
desc = fmt.Sprintf("%s -> %s", diffCopy.FromType, diffCopy.ToType)
}
}
sd.Diffs = sd.Diffs.addDiff(SpecDifference{
DifferenceLocation: location,
Code: diffCopy.Change,
DiffInfo: desc})
}
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 isPrimitiveType(item spec.StringOrArray) bool {
return len(item) > 0 && item[0] != ArrayType && item[0] != ObjectType
}
func isArrayType(item spec.StringOrArray) bool {
return len(item) > 0 && item[0] == ArrayType
}
func (sd *SpecAnalyser) getRefSchemaFromSpec1(ref spec.Ref) (*spec.Schema, string) {
return sd.schemaFromRef(ref, &sd.Definitions1)
}
func (sd *SpecAnalyser) getRefSchemaFromSpec2(ref spec.Ref) (*spec.Schema, string) {
return sd.schemaFromRef(ref, &sd.Definitions2)
}
// CompareSchemaFn Fn spec for comparing schemas
type CompareSchemaFn func(location DifferenceLocation, schema1, schema2 *spec.Schema)
func (sd *SpecAnalyser) compareSchema(location DifferenceLocation, schema1, schema2 *spec.Schema) {
refDiffs := []TypeDiff{}
refDiffs = CheckRefChange(refDiffs, schema1, schema2)
if len(refDiffs) > 0 {
for _, d := range refDiffs {
diff := d
sd.addTypeDiff(location, &diff)
}
return
}
if isRefType(schema1) {
key := schemaLocationKey(location)
if _, ok := sd.schemasCompared[key]; ok {
return
}
sd.schemasCompared[key] = struct{}{}
schema1, _ = sd.schemaFromRef(getRef(schema1), &sd.Definitions1)
}
if isRefType(schema2) {
schema2, _ = sd.schemaFromRef(getRef(schema2), &sd.Definitions2)
}
sd.compareDescripton(location, schema1.Description, schema2.Description)
typeDiffs := sd.CompareProps(&schema1.SchemaProps, &schema2.SchemaProps)
if len(typeDiffs) > 0 {
sd.addDiffs(location, typeDiffs)
return
}
if isArray(schema1) {
if isArray(schema2) {
sd.compareSchema(location, schema1.Items.Schema, schema2.Items.Schema)
} else {
sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: ChangedType, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)}))
}
}
diffs := CompareProperties(location, schema1, schema2, sd.getRefSchemaFromSpec1, sd.getRefSchemaFromSpec2, sd.compareSchema)
for _, diff := range diffs {
sd.Diffs = sd.Diffs.addDiff(diff)
}
}
func (sd *SpecAnalyser) compareSimpleSchema(location DifferenceLocation, schema1, schema2 *spec.SimpleSchema) {
// check optional/required
if schema1.Nullable != schema2.Nullable {
// If optional is made required
if schema1.Nullable && !schema2.Nullable {
sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: ChangedOptionalToRequired, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)}))
} else if !schema1.Nullable && schema2.Nullable {
// If required is made optional
sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: ChangedRequiredToOptional, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)}))
}
}
if schema1.CollectionFormat != schema2.CollectionFormat {
sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: ChangedCollectionFormat, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)}))
}
if schema1.Default != schema2.Default {
switch {
case schema1.Default == nil && schema2.Default != nil:
sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: AddedDefault, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)}))
case schema1.Default != nil && schema2.Default == nil:
sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: DeletedDefault, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)}))
default:
sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: ChangedDefault, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)}))
}
}
if schema1.Example != schema2.Example {
switch {
case schema1.Example == nil && schema2.Example != nil:
sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: AddedExample, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)}))
case schema1.Example != nil && schema2.Example == nil:
sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: DeletedExample, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)}))
default:
sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: ChangedExample, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)}))
}
}
if isArray(schema1) {
if isArray(schema2) {
sd.compareSimpleSchema(location, &schema1.Items.SimpleSchema, &schema2.Items.SimpleSchema)
} else {
sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: ChangedType, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)}))
}
}
}
func (sd *SpecAnalyser) addDiffs(location DifferenceLocation, diffs []TypeDiff) {
for _, e := range diffs {
eachTypeDiff := e
if eachTypeDiff.Change != NoChangeDetected {
sd.addTypeDiff(location, &eachTypeDiff)
}
}
}
func addChildDiffNode(location DifferenceLocation, propName string, propSchema *spec.Schema) DifferenceLocation {
newNode := location.Node
childNode := fromSchemaProps(propName, &propSchema.SchemaProps)
if newNode != nil {
newNode = newNode.Copy()
newNode.AddLeafNode(&childNode)
} else {
newNode = &childNode
}
return DifferenceLocation{
URL: location.URL,
Method: location.Method,
Response: location.Response,
Node: newNode,
}
}
func fromSchemaProps(fieldName string, props *spec.SchemaProps) Node {
node := Node{}
node.TypeName, node.IsArray = getSchemaType(props)
node.Field = fieldName
return node
}
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(ref spec.Ref, defns *spec.Definitions) (actualSchema *spec.Schema, definitionName string) {
definitionName = definitionFromRef(ref)
foundSchema, ok := (*defns)[definitionName]
if !ok {
return nil, definitionName
}
sd.ReferencedDefinitions[definitionName] = true
actualSchema = &foundSchema
return
}
func schemaLocationKey(location DifferenceLocation) string {
return location.Method + location.URL + location.Node.Field + location.Node.TypeName
}
// PropertyDefn combines a property with its required-ness
type PropertyDefn struct {
Schema *spec.Schema
Required bool
}
// PropertyMap a unified map including all AllOf fields
type PropertyMap map[string]PropertyDefn

View file

@ -0,0 +1,216 @@
package diff
import (
"bytes"
"errors"
"fmt"
"io"
"sort"
"strings"
)
// 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
}
// WarningChangeCount Calculates the warning change count
func (sd SpecDifferences) WarningChangeCount() int {
count := 0
for _, eachDiff := range sd {
if eachDiff.Compatibility == Warning {
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 hasMethod {
if hasURL {
prefix = fmt.Sprintf("%s:%s", sd.DifferenceLocation.URL, sd.DifferenceLocation.Method)
}
if isResponse {
prefix += fmt.Sprintf(" -> %d", sd.DifferenceLocation.Response)
direction = "Response"
} else {
direction = "Request"
}
} else {
prefix = sd.DifferenceLocation.URL
}
paramOrPropertyLocation := ""
if sd.DifferenceLocation.Node != nil {
paramOrPropertyLocation = sd.DifferenceLocation.Node.String()
}
optionalInfo := ""
if sd.DiffInfo != "" {
optionalInfo = sd.DiffInfo
}
items := []string{}
for _, item := range []string{prefix, direction, paramOrPropertyLocation, sd.Code.Description(), optionalInfo} {
if item != "" {
items = append(items, item)
}
}
return strings.Join(items, " - ")
// 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() (io.Reader, error, error) {
var out bytes.Buffer
breakingCount := sd.BreakingChangeCount()
if breakingCount > 0 {
if len(*sd) != breakingCount {
fmt.Fprintln(&out, "")
}
fmt.Fprintln(&out, "BREAKING CHANGES:\n=================")
_, _ = out.ReadFrom(sd.reportChanges(Breaking))
msg := fmt.Sprintf("compatibility test FAILED: %d breaking changes detected", breakingCount)
fmt.Fprintln(&out, msg)
return &out, nil, errors.New(msg)
}
fmt.Fprintf(&out, "compatibility test OK. No breaking changes identified.\n")
return &out, nil, nil
}
func (sd SpecDifferences) reportChanges(compat Compatibility) io.Reader {
toReportList := []string{}
var out bytes.Buffer
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.Fprintln(&out, eachDiff)
}
return &out
}
// ReportAllDiffs lists all the diffs between two specs
func (sd SpecDifferences) ReportAllDiffs(fmtJSON bool) (io.Reader, error, error) {
if fmtJSON {
b, err := JSONMarshal(sd)
if err != nil {
return nil, fmt.Errorf("couldn't print results: %v", err), nil
}
out, err := prettyprint(b)
return out, err, nil
}
numDiffs := len(sd)
if numDiffs == 0 {
return bytes.NewBuffer([]byte("No changes identified\n")), nil, nil
}
var out bytes.Buffer
if numDiffs != sd.BreakingChangeCount() {
fmt.Fprintln(&out, "NON-BREAKING CHANGES:\n=====================")
_, _ = out.ReadFrom(sd.reportChanges(NonBreaking))
if sd.WarningChangeCount() > 0 {
fmt.Fprintln(&out, "\nNON-BREAKING CHANGES WITH WARNING:\n==================================")
_, _ = out.ReadFrom(sd.reportChanges(Warning))
}
}
more, err, warn := sd.ReportCompatibility()
if err != nil {
return nil, err, warn
}
_, _ = out.ReadFrom(more)
return &out, nil, warn
}

View file

@ -0,0 +1,163 @@
package diff
import (
"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 forHeader(header spec.Header) *spec.SchemaProps {
return &spec.SchemaProps{
Type: []string{header.Type},
Format: header.Format,
Items: &spec.SchemaOrArray{Schema: forItems(header.Items)},
Maximum: header.Maximum,
ExclusiveMaximum: header.ExclusiveMaximum,
Minimum: header.Minimum,
ExclusiveMinimum: header.ExclusiveMinimum,
MaxLength: header.MaxLength,
MinLength: header.MinLength,
Pattern: header.Pattern,
MaxItems: header.MaxItems,
MinItems: header.MinItems,
UniqueItems: header.UniqueItems,
MultipleOf: header.MultipleOf,
Enum: header.Enum,
}
}
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, eachPath.Extensions}
}
}
return returnURLMethods
}
func isStringType(typeName string) bool {
return typeName == "string" || typeName == "password"
}
// SchemaFromRefFn define this to get a schema for a ref
type SchemaFromRefFn func(spec.Ref) (*spec.Schema, string)
func propertiesFor(schema *spec.Schema, getRefFn SchemaFromRefFn) PropertyMap {
if isRefType(schema) {
schema, _ = getRefFn(schema.Ref)
}
props := PropertyMap{}
requiredProps := schema.Required
requiredMap := map[string]bool{}
for _, prop := range requiredProps {
requiredMap[prop] = true
}
if schema.Properties != nil {
for name, prop := range schema.Properties {
prop := prop
required := requiredMap[name]
props[name] = PropertyDefn{Schema: &prop, Required: required}
}
}
for _, e := range schema.AllOf {
eachAllOf := e
allOfMap := propertiesFor(&eachAllOf, getRefFn)
for name, prop := range allOfMap {
props[name] = prop
}
}
return props
}
func getRef(item interface{}) spec.Ref {
switch s := item.(type) {
case *spec.Refable:
return s.Ref
case *spec.Schema:
return s.Ref
case *spec.SchemaProps:
return s.Ref
default:
return spec.Ref{}
}
}

View file

@ -0,0 +1,81 @@
package commands
import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
"github.com/go-openapi/loads"
"github.com/go-openapi/spec"
"github.com/go-openapi/swag"
flags "github.com/jessevdk/go-flags"
)
// 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"
log.Println("format = ", format)
switch {
case pretty && asJSON:
b, err = json.MarshalIndent(swspec, "", " ")
case asJSON:
b, err = json.Marshal(swspec)
default:
// marshals as YAML
b, err = json.Marshal(swspec)
if err == nil {
var data swag.JSONMapSlice
if erg := json.Unmarshal(b, &data); erg != nil {
log.Fatalln(erg)
}
var bb interface{}
bb, err = data.MarshalYAML()
b = bb.([]byte)
}
}
if err != nil {
return err
}
if output == "" {
fmt.Println(string(b))
return nil
}
return os.WriteFile(output, b, 0644) // #nosec
}

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,29 @@
// 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"`
Cli *generate.Cli `command:"cli"`
Markdown *generate.Markdown `command:"markdown"`
}

View file

@ -0,0 +1,26 @@
package generate
import "github.com/go-swagger/go-swagger/generator"
type Cli struct {
// generate a cli includes all client code
Client
// cmd/<cli-app-name>/main.go will be generated. This ensures that go install will compile the app with desired name.
CliAppName string `long:"cli-app-name" description:"the app name for the cli executable. useful for go install." default:"cli"`
}
func (c Cli) apply(opts *generator.GenOpts) {
c.Client.apply(opts)
opts.IncludeCLi = true
opts.CliPackage = "cli" // hardcoded for now, can be exposed via cmd opt later
opts.CliAppName = c.CliAppName
}
func (c *Cli) generate(opts *generator.GenOpts) error {
return c.Client.generate(opts)
}
// Execute runs this command
func (c *Cli) Execute(args []string) error {
return createSwagger(c)
}

View file

@ -0,0 +1,86 @@
// 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"
)
type clientOptions struct {
ClientPackage string `long:"client-package" short:"c" description:"the package to save the client specific code" default:"client"`
}
func (co clientOptions) apply(opts *generator.GenOpts) {
opts.ClientPackage = co.ClientPackage
}
// Client the command to generate a swagger client
type Client struct {
WithShared
WithModels
WithOperations
clientOptions
schemeOptions
mediaOptions
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"`
Name string `long:"name" short:"A" description:"the name of the application, defaults to a mangled value of info.title"`
}
func (c Client) apply(opts *generator.GenOpts) {
c.Shared.apply(opts)
c.Models.apply(opts)
c.Operations.apply(opts)
c.clientOptions.apply(opts)
c.schemeOptions.apply(opts)
c.mediaOptions.apply(opts)
opts.IncludeModel = !c.SkipModels
opts.IncludeValidator = !c.SkipModels
opts.IncludeHandler = !c.SkipOperations
opts.IncludeParameters = !c.SkipOperations
opts.IncludeResponses = !c.SkipOperations
opts.Name = c.Name
opts.IsClient = true
opts.IncludeSupport = true
}
func (c *Client) generate(opts *generator.GenOpts) error {
return generator.GenerateClient(c.Name, c.Models.Models, c.Operations.Operations, opts)
}
func (c *Client) log(rp string) {
log.Println(`Generation completed!
For this generation to compile you need to have some packages in your go.mod:
* 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 mod tidy`)
}
// Execute runs this command
func (c *Client) Execute(args []string) error {
return createSwagger(c)
}

View file

@ -0,0 +1,17 @@
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) {
// nolint: gocritic
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,33 @@
package generate
import (
"github.com/go-swagger/go-swagger/generator"
"github.com/jessevdk/go-flags"
)
// Markdown generates a markdown representation of the spec
type Markdown struct {
WithShared
WithModels
WithOperations
Output flags.Filename `long:"output" short:"" description:"the file to write the generated markdown." default:"markdown.md"`
}
func (m Markdown) apply(opts *generator.GenOpts) {
m.Shared.apply(opts)
m.Models.apply(opts)
m.Operations.apply(opts)
}
func (m *Markdown) generate(opts *generator.GenOpts) error {
return generator.GenerateMarkdown(string(m.Output), m.Models.Models, m.Operations.Operations, opts)
}
func (m Markdown) log(rp string) {
}
// Execute runs this command
func (m *Markdown) Execute(args []string) error {
return createSwagger(m)
}

View file

@ -0,0 +1,98 @@
// 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"
)
type modelOptions struct {
ModelPackage string `long:"model-package" short:"m" description:"the package to save the models" default:"models"`
Models []string `long:"model" short:"M" description:"specify a model to include in generation, repeat for multiple (defaults to all)"`
ExistingModels string `long:"existing-models" description:"use pre-generated models e.g. github.com/foobar/model"`
StrictAdditionalProperties bool `long:"strict-additional-properties" description:"disallow extra properties when additionalProperties is set to false"`
KeepSpecOrder bool `long:"keep-spec-order" description:"keep schema properties order identical to spec file"`
AllDefinitions bool `long:"all-definitions" description:"generate all model definitions regardless of usage in operations" hidden:"deprecated"`
StructTags []string `long:"struct-tags" description:"the struct tags to generate, repeat for multiple (defaults to json)"`
}
func (mo modelOptions) apply(opts *generator.GenOpts) {
opts.ModelPackage = mo.ModelPackage
opts.Models = mo.Models
opts.ExistingModels = mo.ExistingModels
opts.StrictAdditionalProperties = mo.StrictAdditionalProperties
opts.PropertiesSpecOrder = mo.KeepSpecOrder
opts.IgnoreOperations = mo.AllDefinitions
opts.StructTags = mo.StructTags
}
// WithModels adds the model options group.
//
// This group is available to all commands that need some model generation.
type WithModels struct {
Models modelOptions `group:"Options for model generation"`
}
// Model the generate model file command.
//
// Define the options that are specific to the "swagger generate model" command.
type Model struct {
WithShared
WithModels
NoStruct bool `long:"skip-struct" description:"when present will not generate the model struct" hidden:"deprecated"`
Name []string `long:"name" short:"n" description:"the model to generate, repeat for multiple (defaults to all). Same as --models"`
AcceptDefinitionsOnly bool `long:"accept-definitions-only" description:"accepts a partial swagger spec with only the definitions key"`
}
func (m Model) apply(opts *generator.GenOpts) {
m.Shared.apply(opts)
m.Models.apply(opts)
opts.IncludeModel = !m.NoStruct
opts.IncludeValidator = !m.NoStruct
opts.AcceptDefinitionsOnly = m.AcceptDefinitionsOnly
}
func (m Model) log(rp string) {
log.Println(`Generation completed!
For this generation to compile you need to have some packages in your go.mod:
* github.com/go-openapi/validate
* github.com/go-openapi/strfmt
You can get these now with: go mod tidy`)
}
func (m *Model) generate(opts *generator.GenOpts) error {
return generator.GenerateModels(append(m.Name, m.Models.Models...), opts)
}
// Execute generates a model file
func (m *Model) Execute(args []string) error {
if m.Shared.DumpData && len(append(m.Name, m.Models.Models...)) > 1 {
return errors.New("only 1 model at a time is supported for dumping data")
}
if m.Models.ExistingModels != "" {
log.Println("warning: Ignoring existing-models flag when generating models.")
}
return createSwagger(m)
}

View file

@ -0,0 +1,104 @@
// 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"
)
type operationOptions struct {
Operations []string `long:"operation" short:"O" description:"specify an operation to include, repeat for multiple (defaults to all)"`
Tags []string `long:"tags" description:"the tags to include, if not specified defaults to all" group:"operations"`
APIPackage string `long:"api-package" short:"a" description:"the package to save the operations" default:"operations"`
WithEnumCI bool `long:"with-enum-ci" description:"allow case-insensitive enumerations"`
// tags handling
SkipTagPackages bool `long:"skip-tag-packages" description:"skips the generation of tag-based operation packages, resulting in a flat generation"`
}
func (oo operationOptions) apply(opts *generator.GenOpts) {
opts.Operations = oo.Operations
opts.Tags = oo.Tags
opts.APIPackage = oo.APIPackage
opts.AllowEnumCI = oo.WithEnumCI
opts.SkipTagPackages = oo.SkipTagPackages
}
// WithOperations adds the operations options group
type WithOperations struct {
Operations operationOptions `group:"Options for operation generation"`
}
// Operation the generate operation files command
type Operation struct {
WithShared
WithOperations
clientOptions
serverOptions
schemeOptions
mediaOptions
ModelPackage string `long:"model-package" short:"m" description:"the package to save the models" default:"models"`
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"`
Name []string `long:"name" short:"n" description:"the operations to generate, repeat for multiple (defaults to all). Same as --operations"`
}
func (o Operation) apply(opts *generator.GenOpts) {
o.Shared.apply(opts)
o.Operations.apply(opts)
o.clientOptions.apply(opts)
o.serverOptions.apply(opts)
o.schemeOptions.apply(opts)
o.mediaOptions.apply(opts)
opts.ModelPackage = o.ModelPackage
opts.IncludeHandler = !o.NoHandler
opts.IncludeResponses = !o.NoResponses
opts.IncludeParameters = !o.NoStruct
opts.IncludeURLBuilder = !o.NoURLBuilder
}
func (o *Operation) generate(opts *generator.GenOpts) error {
return generator.GenerateServerOperation(append(o.Name, o.Operations.Operations...), opts)
}
func (o Operation) log(rp string) {
log.Println(`Generation completed!
For this generation to compile you need to have some packages in your go.mod:
* github.com/go-openapi/runtime
You can get these now with: go mod tidy`)
}
// Execute generates a model file
func (o *Operation) Execute(args []string) error {
if o.Shared.DumpData && len(append(o.Name, o.Operations.Operations...)) > 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"
)
type serverOptions struct {
ServerPackage string `long:"server-package" short:"s" description:"the package to save the server specific code" default:"restapi"`
MainTarget string `long:"main-package" short:"" description:"the location of the generated main. Defaults to cmd/{name}-server" default:""`
ImplementationPackage string `long:"implementation-package" short:"" description:"the location of the backend implementation of the server, which will be autowired with api" default:""`
}
func (cs serverOptions) apply(opts *generator.GenOpts) {
opts.ServerPackage = cs.ServerPackage
}
// Server the command to generate an entire server application
type Server struct {
WithShared
WithModels
WithOperations
serverOptions
schemeOptions
mediaOptions
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"`
FlagStrategy string `long:"flag-strategy" description:"the strategy to provide flags for the server" default:"go-flags" choice:"go-flags" choice:"pflag" choice:"flag"` // nolint: staticcheck
CompatibilityMode string `long:"compatibility-mode" description:"the compatibility mode for the tls server" default:"modern" choice:"modern" choice:"intermediate"` // nolint: staticcheck
RegenerateConfigureAPI bool `long:"regenerate-configureapi" description:"Force regeneration of configureapi.go"`
Name string `long:"name" short:"A" description:"the name of the application, defaults to a mangled value of info.title"`
// TODO(fredbi): CmdName string `long:"cmd-name" short:"A" description:"the name of the server command, when main is generated (defaults to {name}-server)"`
// deprecated flags
WithContext bool `long:"with-context" description:"handlers get a context as first arg (deprecated)"`
}
func (s Server) apply(opts *generator.GenOpts) {
if s.WithContext {
log.Printf("warning: deprecated option --with-context is ignored")
}
s.Shared.apply(opts)
s.Models.apply(opts)
s.Operations.apply(opts)
s.serverOptions.apply(opts)
s.schemeOptions.apply(opts)
s.mediaOptions.apply(opts)
opts.IncludeModel = !s.SkipModels
opts.IncludeValidator = !s.SkipModels
opts.IncludeHandler = !s.SkipOperations
opts.IncludeParameters = !s.SkipOperations
opts.IncludeResponses = !s.SkipOperations
opts.IncludeURLBuilder = !s.SkipOperations
opts.IncludeSupport = !s.SkipSupport
opts.IncludeMain = !s.ExcludeMain
opts.ExcludeSpec = s.ExcludeSpec
opts.FlagStrategy = s.FlagStrategy
opts.CompatibilityMode = s.CompatibilityMode
opts.RegenerateConfigureAPI = s.RegenerateConfigureAPI
opts.Name = s.Name
opts.MainPackage = s.MainTarget
opts.ImplementationPackage = s.ImplementationPackage
}
func (s *Server) generate(opts *generator.GenOpts) error {
return generator.GenerateServer(s.Name, s.Models.Models, s.Operations.Operations, opts)
}
func (s Server) log(rp string) {
var flagsPackage string
switch {
case strings.HasPrefix(s.FlagStrategy, "pflag"):
flagsPackage = "github.com/spf13/pflag"
case strings.HasPrefix(s.FlagStrategy, "flag"):
flagsPackage = "flag"
default:
flagsPackage = "github.com/jessevdk/go-flags"
}
log.Println(`Generation completed!
For this generation to compile you need to have some packages in your go.mod:
* github.com/go-openapi/runtime
* ` + flagsPackage + `
You can get these now with: go mod tidy`)
}
// Execute runs this command
func (s *Server) Execute(args []string) error {
return createSwagger(s)
}

View file

@ -0,0 +1,240 @@
package generate
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"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)" group:"shared"`
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" group:"shared"` // nolint: staticcheck
}
// 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
expandIsSet := false
if f.WithExpand {
res.Expand = true
expandIsSet = true
}
for _, opt := range f.WithFlatten {
switch opt {
case "verbose":
res.Verbose = true
verboseIsSet = true
case "noverbose":
if !verboseIsSet {
// verbose flag takes precedence
res.Verbose = false
verboseIsSet = true
}
case "remove-unused":
res.RemoveUnused = true
case "expand":
res.Expand = true
expandIsSet = true
case "full":
if !minimalIsSet && !expandIsSet {
// minimal flag takes precedence
res.Minimal = false
minimalIsSet = true
}
case "minimal":
if !expandIsSet {
// expand flag takes precedence
res.Minimal = true
minimalIsSet = true
}
}
}
return
}
type sharedCommand interface {
apply(*generator.GenOpts)
getConfigFile() string
generate(*generator.GenOpts) error
log(string)
}
type schemeOptions struct {
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"`
PrincipalIface bool `long:"principal-is-interface" description:"the security principal provided is an interface, not a struct"`
}
func (so schemeOptions) apply(opts *generator.GenOpts) {
opts.Principal = so.Principal
opts.PrincipalCustomIface = so.PrincipalIface
opts.DefaultScheme = so.DefaultScheme
}
type mediaOptions struct {
DefaultProduces string `long:"default-produces" description:"the default mime type that API operations produce" default:"application/json"`
DefaultConsumes string `long:"default-consumes" description:"the default mime type that API operations consume" default:"application/json"`
}
func (m mediaOptions) apply(opts *generator.GenOpts) {
opts.DefaultProduces = m.DefaultProduces
opts.DefaultConsumes = m.DefaultConsumes
const xmlIdentifier = "xml"
opts.WithXML = strings.Contains(opts.DefaultProduces, xmlIdentifier) || strings.Contains(opts.DefaultConsumes, xmlIdentifier)
}
// WithShared adds the shared options group
type WithShared struct {
Shared sharedOptions `group:"Options common to all code generation commands"`
}
func (w WithShared) getConfigFile() string {
return string(w.Shared.ConfigFile)
}
type sharedOptionsCommon struct {
Spec flags.Filename `long:"spec" short:"f" description:"the spec file to use (default swagger.{json,yml,yaml})" group:"shared"`
Target flags.Filename `long:"target" short:"t" default:"./" description:"the base directory for generating the files" group:"shared"`
Template string `long:"template" description:"load contributed templates" choice:"stratoscale" group:"shared"`
TemplateDir flags.Filename `long:"template-dir" short:"T" description:"alternative template override directory" group:"shared"`
ConfigFile flags.Filename `long:"config-file" short:"C" description:"configuration file to use for overriding template options" group:"shared"`
CopyrightFile flags.Filename `long:"copyright-file" short:"r" description:"copyright file used to add copyright header" group:"shared"`
AdditionalInitialisms []string `long:"additional-initialism" description:"consecutive capitals that should be considered intialisms" group:"shared"`
AllowTemplateOverride bool `long:"allow-template-override" description:"allows overriding protected templates" group:"shared"`
SkipValidation bool `long:"skip-validation" description:"skips validation of spec prior to generation" group:"shared"`
DumpData bool `long:"dump-data" description:"when present dumps the json for the template generator instead of generating files" group:"shared"`
StrictResponders bool `long:"strict-responders" description:"Use strict type for the handler return value"`
FlattenCmdOptions
}
func (s sharedOptionsCommon) apply(opts *generator.GenOpts) {
opts.Spec = string(s.Spec)
opts.Target = string(s.Target)
opts.Template = s.Template
opts.TemplateDir = string(s.TemplateDir)
opts.AllowTemplateOverride = s.AllowTemplateOverride
opts.ValidateSpec = !s.SkipValidation
opts.DumpData = s.DumpData
opts.FlattenOpts = s.FlattenCmdOptions.SetFlattenOptions(opts.FlattenOpts)
opts.Copyright = string(s.CopyrightFile)
opts.StrictResponders = s.StrictResponders
swag.AddInitialisms(s.AdditionalInitialisms...)
}
func setCopyright(copyrightFile string) (string, error) {
// read the Copyright from file path in opts
if copyrightFile == "" {
return "", nil
}
bytebuffer, err := os.ReadFile(copyrightFile)
if err != nil {
return "", err
}
return string(bytebuffer), nil
}
func createSwagger(s sharedCommand) error {
cfg, err := readConfig(s.getConfigFile())
if err != nil {
return err
}
setDebug(cfg) // viper config Debug
opts := new(generator.GenOpts)
s.apply(opts)
opts.Copyright, err = setCopyright(opts.Copyright)
if err != nil {
return fmt.Errorf("could not load copyright file: %v", err)
}
if opts.Template != "" {
contribOptionsOverride(opts)
}
if err = opts.EnsureDefaults(); err != nil {
return err
}
if err = configureOptsFromConfig(cfg, opts); err != nil {
return err
}
if err = s.generate(opts); err != nil {
return err
}
basepath, err := filepath.Abs(".")
if err != nil {
return err
}
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) {
// viper config debug
if os.Getenv("DEBUG") != "" || os.Getenv("SWAGGER_DEBUG") != "" {
if cfg != nil {
cfg.Debug()
} else {
log.Println("No config read")
}
}
}

View file

@ -0,0 +1,19 @@
//go:build !windows
// +build !windows
package generate
import (
"github.com/go-swagger/go-swagger/generator"
"github.com/jessevdk/go-flags"
)
type sharedOptions struct {
sharedOptionsCommon
TemplatePlugin flags.Filename `long:"template-plugin" short:"p" description:"the template plugin to use" group:"shared"`
}
func (s sharedOptions) apply(opts *generator.GenOpts) {
opts.TemplatePlugin = string(s.TemplatePlugin)
s.sharedOptionsCommon.apply(opts)
}

View file

@ -0,0 +1,8 @@
//go:build windows
// +build windows
package generate
type sharedOptions struct {
sharedOptionsCommon
}

View file

@ -0,0 +1,125 @@
//go:build !go1.11
// +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"
"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.v3"
)
// 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:"an input swagger file with which to merge"`
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 os.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 @@
//go:build go1.11
// +build go1.11
package generate
import (
"encoding/json"
"fmt"
"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.v3"
)
// 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:"an input swagger file with which to merge"`
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 os.WriteFile(output, b, 0644) // #nosec
}
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,67 @@
// 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 {
WithShared
WithModels
WithOperations
clientOptions
serverOptions
schemeOptions
mediaOptions
Name string `long:"name" short:"A" description:"the name of the application, defaults to a mangled value of info.title"`
}
func (s *Support) apply(opts *generator.GenOpts) {
s.Shared.apply(opts)
s.Models.apply(opts)
s.Operations.apply(opts)
s.clientOptions.apply(opts)
s.serverOptions.apply(opts)
s.schemeOptions.apply(opts)
s.mediaOptions.apply(opts)
}
func (s *Support) generate(opts *generator.GenOpts) error {
return generator.GenerateSupport(s.Name, s.Models.Models, s.Operations.Operations, opts)
}
func (s Support) log(rp string) {
log.Println(`Generation completed!
For this generation to compile you need to have some packages in go.mod:
* github.com/go-openapi/runtime
* github.com/asaskevich/govalidator
* github.com/jessevdk/go-flags
You can get these now with: go mod tidy`)
}
// 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.v3"
"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,117 @@
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"
"github.com/go-swagger/go-swagger/generator"
)
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...>"
ignoreConflictsAndCollisionsSpecified = "both the flags ignore conflicts and collisions were specified. These have conflicting meaning so please only specify one"
)
// 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"`
KeepSpecOrder bool `long:"keep-spec-order" description:"Keep schema properties order identical to spec file"`
Format string `long:"format" description:"the format for the spec document" default:"json" choice:"yaml" choice:"json"`
IgnoreConflicts bool `long:"ignore-conflicts" description:"Ignore conflict"`
}
// 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)
}
if c.IgnoreConflicts && c.ExpectedCollisionCount != 0 {
return errors.New(ignoreConflictsAndCollisionsSpecified)
}
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 c.IgnoreConflicts {
return nil
}
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 {
if c.KeepSpecOrder {
mixinFile = generator.WithAutoXOrder(mixinFile)
}
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,117 @@
package commands
import (
"encoding/json"
"errors"
"fmt"
"log"
"net"
"net/http"
"path"
"strconv"
"github.com/go-openapi/loads"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/spec"
"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"`
Flatten bool `long:"flatten" description:"when present, flatten the swagger spec before serving it"`
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" default:"0.0.0.0" env:"HOST"`
Path string `long:"path" description:"the uri path at which the docs will be served" default:"docs"`
}
// 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
}
if s.Flatten {
specDoc, err = specDoc.Expanded(&spec.ExpandOptions{
SkipSchemas: false,
ContinueOnError: true,
AbsoluteCircularRef: true,
})
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: s.Path,
}, handler)
visit = fmt.Sprintf("http://%s:%d%s", sh, sp, path.Join(basePath, "docs"))
} else if visit != "" || s.Flavor == "swagger" {
handler = middleware.SwaggerUI(middleware.SwaggerUIOpts{
BasePath: basePath,
SpecURL: path.Join(basePath, "swagger.json"),
Path: s.Path,
}, handler)
visit = fmt.Sprintf("http://%s:%d%s", sh, sp, path.Join(basePath, s.Path))
}
}
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,37 @@
package commands
import (
"fmt"
"runtime/debug"
)
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 == "" {
if info, available := debug.ReadBuildInfo(); available && info.Main.Version != "(devel)" {
// built from source, with module (e.g. go get)
fmt.Println("version:", info.Main.Version)
fmt.Println("commit:", fmt.Sprintf("(unknown, mod sum: %q)", info.Main.Sum))
return nil
}
// built from source, local repo
fmt.Println("dev")
return nil
}
// released version
fmt.Println("version:", Version)
fmt.Println("commit:", Commit)
return nil
}

View file

@ -0,0 +1,143 @@
// 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"
"log"
"os"
"github.com/go-swagger/go-swagger/cmd/swagger/commands"
flags "github.com/jessevdk/go-flags"
)
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
case "markdown":
cmd.ShortDescription = "generate a markdown representation from the swagger spec"
cmd.LongDescription = cmd.ShortDescription
case "cli":
cmd.ShortDescription = "generate a command line client tool from the swagger spec"
cmd.LongDescription = cmd.ShortDescription
}
}
opts.Quiet = func() {
log.SetOutput(io.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)
}
}