[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

202
vendor/github.com/go-swagger/go-swagger/LICENSE generated vendored Normal file
View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

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)
}
}

View file

@ -0,0 +1,3 @@
# codescan
Version of the go source parser with support for go modules, from go1.11 onwards.

View file

@ -0,0 +1,674 @@
package codescan
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"log"
"os"
"strings"
"github.com/go-openapi/swag"
"golang.org/x/tools/go/packages"
"github.com/go-openapi/spec"
)
const pkgLoadMode = packages.NeedName | packages.NeedFiles | packages.NeedImports | packages.NeedDeps | packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo
func safeConvert(str string) bool {
b, err := swag.ConvertBool(str)
if err != nil {
return false
}
return b
}
// Debug is true when process is run with DEBUG=1 env var
var Debug = safeConvert(os.Getenv("DEBUG"))
type node uint32
const (
metaNode node = 1 << iota
routeNode
operationNode
modelNode
parametersNode
responseNode
)
// Options for the scanner
type Options struct {
Packages []string
InputSpec *spec.Swagger
ScanModels bool
WorkDir string
BuildTags string
ExcludeDeps bool
Include []string
Exclude []string
IncludeTags []string
ExcludeTags []string
}
type scanCtx struct {
pkgs []*packages.Package
app *typeIndex
}
func sliceToSet(names []string) map[string]bool {
result := make(map[string]bool)
for _, v := range names {
result[v] = true
}
return result
}
// Run the scanner to produce a spec with the options provided
func Run(opts *Options) (*spec.Swagger, error) {
sc, err := newScanCtx(opts)
if err != nil {
return nil, err
}
sb := newSpecBuilder(opts.InputSpec, sc, opts.ScanModels)
return sb.Build()
}
func newScanCtx(opts *Options) (*scanCtx, error) {
cfg := &packages.Config{
Dir: opts.WorkDir,
Mode: pkgLoadMode,
Tests: false,
}
if opts.BuildTags != "" {
cfg.BuildFlags = []string{"-tags", opts.BuildTags}
}
pkgs, err := packages.Load(cfg, opts.Packages...)
if err != nil {
return nil, err
}
app, err := newTypeIndex(pkgs, opts.ExcludeDeps,
sliceToSet(opts.IncludeTags), sliceToSet(opts.ExcludeTags),
opts.Include, opts.Exclude)
if err != nil {
return nil, err
}
return &scanCtx{
pkgs: pkgs,
app: app,
}, nil
}
type entityDecl struct {
Comments *ast.CommentGroup
Type *types.Named
Ident *ast.Ident
Spec *ast.TypeSpec
File *ast.File
Pkg *packages.Package
hasModelAnnotation bool
hasResponseAnnotation bool
hasParameterAnnotation bool
}
func (d *entityDecl) Names() (name, goName string) {
goName = d.Ident.Name
name = goName
if d.Comments == nil {
return
}
DECLS:
for _, cmt := range d.Comments.List {
for _, ln := range strings.Split(cmt.Text, "\n") {
matches := rxModelOverride.FindStringSubmatch(ln)
if len(matches) > 0 {
d.hasModelAnnotation = true
}
if len(matches) > 1 && len(matches[1]) > 0 {
name = matches[1]
break DECLS
}
}
}
return
}
func (d *entityDecl) ResponseNames() (name, goName string) {
goName = d.Ident.Name
name = goName
if d.Comments == nil {
return
}
DECLS:
for _, cmt := range d.Comments.List {
for _, ln := range strings.Split(cmt.Text, "\n") {
matches := rxResponseOverride.FindStringSubmatch(ln)
if len(matches) > 0 {
d.hasResponseAnnotation = true
}
if len(matches) > 1 && len(matches[1]) > 0 {
name = matches[1]
break DECLS
}
}
}
return
}
func (d *entityDecl) OperationIDS() (result []string) {
if d == nil || d.Comments == nil {
return nil
}
for _, cmt := range d.Comments.List {
for _, ln := range strings.Split(cmt.Text, "\n") {
matches := rxParametersOverride.FindStringSubmatch(ln)
if len(matches) > 0 {
d.hasParameterAnnotation = true
}
if len(matches) > 1 && len(matches[1]) > 0 {
for _, pt := range strings.Split(matches[1], " ") {
tr := strings.TrimSpace(pt)
if len(tr) > 0 {
result = append(result, tr)
}
}
}
}
}
return
}
func (d *entityDecl) HasModelAnnotation() bool {
if d.hasModelAnnotation {
return true
}
if d.Comments == nil {
return false
}
for _, cmt := range d.Comments.List {
for _, ln := range strings.Split(cmt.Text, "\n") {
matches := rxModelOverride.FindStringSubmatch(ln)
if len(matches) > 0 {
d.hasModelAnnotation = true
return true
}
}
}
return false
}
func (d *entityDecl) HasResponseAnnotation() bool {
if d.hasResponseAnnotation {
return true
}
if d.Comments == nil {
return false
}
for _, cmt := range d.Comments.List {
for _, ln := range strings.Split(cmt.Text, "\n") {
matches := rxResponseOverride.FindStringSubmatch(ln)
if len(matches) > 0 {
d.hasResponseAnnotation = true
return true
}
}
}
return false
}
func (d *entityDecl) HasParameterAnnotation() bool {
if d.hasParameterAnnotation {
return true
}
if d.Comments == nil {
return false
}
for _, cmt := range d.Comments.List {
for _, ln := range strings.Split(cmt.Text, "\n") {
matches := rxParametersOverride.FindStringSubmatch(ln)
if len(matches) > 0 {
d.hasParameterAnnotation = true
return true
}
}
}
return false
}
func (s *scanCtx) FindDecl(pkgPath, name string) (*entityDecl, bool) {
if pkg, ok := s.app.AllPackages[pkgPath]; ok {
for _, file := range pkg.Syntax {
for _, d := range file.Decls {
gd, ok := d.(*ast.GenDecl)
if !ok {
continue
}
for _, sp := range gd.Specs {
if ts, ok := sp.(*ast.TypeSpec); ok && ts.Name.Name == name {
def, ok := pkg.TypesInfo.Defs[ts.Name]
if !ok {
debugLog("couldn't find type info for %s", ts.Name)
continue
}
nt, isNamed := def.Type().(*types.Named)
if !isNamed {
debugLog("%s is not a named type but a %T", ts.Name, def.Type())
continue
}
comments := ts.Doc // type ( /* doc */ Foo struct{} )
if comments == nil {
comments = gd.Doc // /* doc */ type ( Foo struct{} )
}
decl := &entityDecl{
Comments: comments,
Type: nt,
Ident: ts.Name,
Spec: ts,
File: file,
Pkg: pkg,
}
return decl, true
}
}
}
}
}
return nil, false
}
func (s *scanCtx) FindModel(pkgPath, name string) (*entityDecl, bool) {
for _, cand := range s.app.Models {
ct := cand.Type.Obj()
if ct.Name() == name && ct.Pkg().Path() == pkgPath {
return cand, true
}
}
if decl, found := s.FindDecl(pkgPath, name); found {
s.app.ExtraModels[decl.Ident] = decl
return decl, true
}
return nil, false
}
func (s *scanCtx) PkgForPath(pkgPath string) (*packages.Package, bool) {
v, ok := s.app.AllPackages[pkgPath]
return v, ok
}
func (s *scanCtx) DeclForType(t types.Type) (*entityDecl, bool) {
switch tpe := t.(type) {
case *types.Pointer:
return s.DeclForType(tpe.Elem())
case *types.Named:
return s.FindDecl(tpe.Obj().Pkg().Path(), tpe.Obj().Name())
default:
log.Printf("unknown type to find the package for [%T]: %s", t, t.String())
return nil, false
}
}
func (s *scanCtx) PkgForType(t types.Type) (*packages.Package, bool) {
switch tpe := t.(type) {
// case *types.Basic:
// case *types.Struct:
// case *types.Pointer:
// case *types.Interface:
// case *types.Array:
// case *types.Slice:
// case *types.Map:
case *types.Named:
v, ok := s.app.AllPackages[tpe.Obj().Pkg().Path()]
return v, ok
default:
log.Printf("unknown type to find the package for [%T]: %s", t, t.String())
return nil, false
}
}
func (s *scanCtx) FindComments(pkg *packages.Package, name string) (*ast.CommentGroup, bool) {
for _, f := range pkg.Syntax {
for _, d := range f.Decls {
gd, ok := d.(*ast.GenDecl)
if !ok {
continue
}
for _, s := range gd.Specs {
if ts, ok := s.(*ast.TypeSpec); ok {
if ts.Name.Name == name {
return gd.Doc, true
}
}
}
}
}
return nil, false
}
func (s *scanCtx) FindEnumValues(pkg *packages.Package, enumName string) (list []interface{}, descList []string, _ bool) {
for _, f := range pkg.Syntax {
for _, d := range f.Decls {
gd, ok := d.(*ast.GenDecl)
if !ok {
continue
}
if gd.Tok != token.CONST {
continue
}
for _, s := range gd.Specs {
if vs, ok := s.(*ast.ValueSpec); ok {
if vsIdent, ok := vs.Type.(*ast.Ident); ok {
if vsIdent.Name == enumName {
if len(vs.Values) > 0 {
if bl, ok := vs.Values[0].(*ast.BasicLit); ok {
blValue := getEnumBasicLitValue(bl)
list = append(list, blValue)
// build the enum description
var (
desc = &strings.Builder{}
namesLen = len(vs.Names)
)
desc.WriteString(fmt.Sprintf("%v ", blValue))
for i, name := range vs.Names {
desc.WriteString(name.Name)
if i < namesLen-1 {
desc.WriteString(" ")
}
}
if vs.Doc != nil {
docListLen := len(vs.Doc.List)
if docListLen > 0 {
desc.WriteString(" ")
}
for i, doc := range vs.Doc.List {
if doc.Text != "" {
var text = strings.TrimPrefix(doc.Text, "//")
desc.WriteString(text)
if i < docListLen-1 {
desc.WriteString(" ")
}
}
}
}
descList = append(descList, desc.String())
}
}
}
}
}
}
}
}
return list, descList, true
}
func newTypeIndex(pkgs []*packages.Package,
excludeDeps bool, includeTags, excludeTags map[string]bool,
includePkgs, excludePkgs []string) (*typeIndex, error) {
ac := &typeIndex{
AllPackages: make(map[string]*packages.Package),
Models: make(map[*ast.Ident]*entityDecl),
ExtraModels: make(map[*ast.Ident]*entityDecl),
excludeDeps: excludeDeps,
includeTags: includeTags,
excludeTags: excludeTags,
includePkgs: includePkgs,
excludePkgs: excludePkgs,
}
if err := ac.build(pkgs); err != nil {
return nil, err
}
return ac, nil
}
type typeIndex struct {
AllPackages map[string]*packages.Package
Models map[*ast.Ident]*entityDecl
ExtraModels map[*ast.Ident]*entityDecl
Meta []metaSection
Routes []parsedPathContent
Operations []parsedPathContent
Parameters []*entityDecl
Responses []*entityDecl
excludeDeps bool
includeTags map[string]bool
excludeTags map[string]bool
includePkgs []string
excludePkgs []string
}
func (a *typeIndex) build(pkgs []*packages.Package) error {
for _, pkg := range pkgs {
if _, known := a.AllPackages[pkg.PkgPath]; known {
continue
}
a.AllPackages[pkg.PkgPath] = pkg
if err := a.processPackage(pkg); err != nil {
return err
}
if err := a.walkImports(pkg); err != nil {
return err
}
}
return nil
}
func (a *typeIndex) processPackage(pkg *packages.Package) error {
if !shouldAcceptPkg(pkg.PkgPath, a.includePkgs, a.excludePkgs) {
debugLog("package %s is ignored due to rules", pkg.Name)
return nil
}
for _, file := range pkg.Syntax {
n, err := a.detectNodes(file)
if err != nil {
return err
}
if n&metaNode != 0 {
a.Meta = append(a.Meta, metaSection{Comments: file.Doc})
}
if n&operationNode != 0 {
for _, cmts := range file.Comments {
pp := parsePathAnnotation(rxOperation, cmts.List)
if pp.Method == "" {
continue // not a valid operation
}
if !shouldAcceptTag(pp.Tags, a.includeTags, a.excludeTags) {
debugLog("operation %s %s is ignored due to tag rules", pp.Method, pp.Path)
continue
}
a.Operations = append(a.Operations, pp)
}
}
if n&routeNode != 0 {
for _, cmts := range file.Comments {
pp := parsePathAnnotation(rxRoute, cmts.List)
if pp.Method == "" {
continue // not a valid operation
}
if !shouldAcceptTag(pp.Tags, a.includeTags, a.excludeTags) {
debugLog("operation %s %s is ignored due to tag rules", pp.Method, pp.Path)
continue
}
a.Routes = append(a.Routes, pp)
}
}
for _, dt := range file.Decls {
switch fd := dt.(type) {
case *ast.BadDecl:
continue
case *ast.FuncDecl:
if fd.Body == nil {
continue
}
for _, stmt := range fd.Body.List {
if dstm, ok := stmt.(*ast.DeclStmt); ok {
if gd, isGD := dstm.Decl.(*ast.GenDecl); isGD {
a.processDecl(pkg, file, n, gd)
}
}
}
case *ast.GenDecl:
a.processDecl(pkg, file, n, fd)
}
}
}
return nil
}
func (a *typeIndex) processDecl(pkg *packages.Package, file *ast.File, n node, gd *ast.GenDecl) {
for _, sp := range gd.Specs {
switch ts := sp.(type) {
case *ast.ValueSpec:
debugLog("saw value spec: %v", ts.Names)
return
case *ast.ImportSpec:
debugLog("saw import spec: %v", ts.Name)
return
case *ast.TypeSpec:
def, ok := pkg.TypesInfo.Defs[ts.Name]
if !ok {
debugLog("couldn't find type info for %s", ts.Name)
continue
}
nt, isNamed := def.Type().(*types.Named)
if !isNamed {
debugLog("%s is not a named type but a %T", ts.Name, def.Type())
continue
}
comments := ts.Doc // type ( /* doc */ Foo struct{} )
if comments == nil {
comments = gd.Doc // /* doc */ type ( Foo struct{} )
}
decl := &entityDecl{
Comments: comments,
Type: nt,
Ident: ts.Name,
Spec: ts,
File: file,
Pkg: pkg,
}
key := ts.Name
if n&modelNode != 0 && decl.HasModelAnnotation() {
a.Models[key] = decl
}
if n&parametersNode != 0 && decl.HasParameterAnnotation() {
a.Parameters = append(a.Parameters, decl)
}
if n&responseNode != 0 && decl.HasResponseAnnotation() {
a.Responses = append(a.Responses, decl)
}
}
}
}
func (a *typeIndex) walkImports(pkg *packages.Package) error {
if a.excludeDeps {
return nil
}
for _, v := range pkg.Imports {
if _, known := a.AllPackages[v.PkgPath]; known {
continue
}
a.AllPackages[v.PkgPath] = v
if err := a.processPackage(v); err != nil {
return err
}
if err := a.walkImports(v); err != nil {
return err
}
}
return nil
}
func (a *typeIndex) detectNodes(file *ast.File) (node, error) {
var n node
for _, comments := range file.Comments {
var seenStruct string
for _, cline := range comments.List {
if cline == nil {
continue
}
}
for _, cline := range comments.List {
if cline == nil {
continue
}
matches := rxSwaggerAnnotation.FindStringSubmatch(cline.Text)
if len(matches) < 2 {
continue
}
switch matches[1] {
case "route":
n |= routeNode
case "operation":
n |= operationNode
case "model":
n |= modelNode
if seenStruct == "" || seenStruct == matches[1] {
seenStruct = matches[1]
} else {
return 0, fmt.Errorf("classifier: already annotated as %s, can't also be %q - %s", seenStruct, matches[1], cline.Text)
}
case "meta":
n |= metaNode
case "parameters":
n |= parametersNode
if seenStruct == "" || seenStruct == matches[1] {
seenStruct = matches[1]
} else {
return 0, fmt.Errorf("classifier: already annotated as %s, can't also be %q - %s", seenStruct, matches[1], cline.Text)
}
case "response":
n |= responseNode
if seenStruct == "" || seenStruct == matches[1] {
seenStruct = matches[1]
} else {
return 0, fmt.Errorf("classifier: already annotated as %s, can't also be %q - %s", seenStruct, matches[1], cline.Text)
}
case "strfmt", "name", "discriminated", "file", "enum", "default", "alias", "type":
// TODO: perhaps collect these and pass along to avoid lookups later on
case "allOf":
case "ignore":
default:
return 0, fmt.Errorf("classifier: unknown swagger annotation %q", matches[1])
}
}
}
return n, nil
}
func debugLog(format string, args ...interface{}) {
if Debug {
log.Printf(format, args...)
}
}

View file

@ -0,0 +1,6 @@
/*
Package codescan provides a scanner for go files that produces a swagger spec document.
This package is intendnd for go1.11 onwards, and does support go modules.
*/
package codescan

View file

@ -0,0 +1,32 @@
package codescan
import (
"go/ast"
"strconv"
"strings"
"github.com/go-openapi/spec"
)
func getEnumBasicLitValue(basicLit *ast.BasicLit) interface{} {
switch basicLit.Kind.String() {
case "INT":
if result, err := strconv.ParseInt(basicLit.Value, 10, 64); err == nil {
return result
}
case "FLOAT":
if result, err := strconv.ParseFloat(basicLit.Value, 64); err == nil {
return result
}
default:
return strings.Trim(basicLit.Value, "\"")
}
return nil
}
const extEnumDesc = "x-go-enum-desc"
func getEnumDesc(extensions spec.Extensions) (desc string) {
desc, _ = extensions.GetString(extEnumDesc)
return
}

View file

@ -0,0 +1,252 @@
// 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 codescan
import (
"encoding/json"
"fmt"
"go/ast"
"net/mail"
"regexp"
"strings"
"github.com/go-openapi/spec"
)
type metaSection struct {
Comments *ast.CommentGroup
}
func metaTOSSetter(meta *spec.Info) func([]string) {
return func(lines []string) {
meta.TermsOfService = joinDropLast(lines)
}
}
func metaConsumesSetter(meta *spec.Swagger) func([]string) {
return func(consumes []string) { meta.Consumes = consumes }
}
func metaProducesSetter(meta *spec.Swagger) func([]string) {
return func(produces []string) { meta.Produces = produces }
}
func metaSchemeSetter(meta *spec.Swagger) func([]string) {
return func(schemes []string) { meta.Schemes = schemes }
}
func metaSecuritySetter(meta *spec.Swagger) func([]map[string][]string) {
return func(secDefs []map[string][]string) { meta.Security = secDefs }
}
func metaSecurityDefinitionsSetter(meta *spec.Swagger) func(json.RawMessage) error {
return func(jsonValue json.RawMessage) error {
var jsonData spec.SecurityDefinitions
err := json.Unmarshal(jsonValue, &jsonData)
if err != nil {
return err
}
meta.SecurityDefinitions = jsonData
return nil
}
}
func metaVendorExtensibleSetter(meta *spec.Swagger) func(json.RawMessage) error {
return func(jsonValue json.RawMessage) error {
var jsonData spec.Extensions
err := json.Unmarshal(jsonValue, &jsonData)
if err != nil {
return err
}
for k := range jsonData {
if !rxAllowedExtensions.MatchString(k) {
return fmt.Errorf("invalid schema extension name, should start from `x-`: %s", k)
}
}
meta.Extensions = jsonData
return nil
}
}
func infoVendorExtensibleSetter(meta *spec.Swagger) func(json.RawMessage) error {
return func(jsonValue json.RawMessage) error {
var jsonData spec.Extensions
err := json.Unmarshal(jsonValue, &jsonData)
if err != nil {
return err
}
for k := range jsonData {
if !rxAllowedExtensions.MatchString(k) {
return fmt.Errorf("invalid schema extension name, should start from `x-`: %s", k)
}
}
meta.Info.Extensions = jsonData
return nil
}
}
func newMetaParser(swspec *spec.Swagger) *sectionedParser {
sp := new(sectionedParser)
if swspec.Info == nil {
swspec.Info = new(spec.Info)
}
info := swspec.Info
sp.setTitle = func(lines []string) {
tosave := joinDropLast(lines)
if len(tosave) > 0 {
tosave = rxStripTitleComments.ReplaceAllString(tosave, "")
}
info.Title = tosave
}
sp.setDescription = func(lines []string) { info.Description = joinDropLast(lines) }
sp.taggers = []tagParser{
newMultiLineTagParser("TOS", newMultilineDropEmptyParser(rxTOS, metaTOSSetter(info)), false),
newMultiLineTagParser("Consumes", newMultilineDropEmptyParser(rxConsumes, metaConsumesSetter(swspec)), false),
newMultiLineTagParser("Produces", newMultilineDropEmptyParser(rxProduces, metaProducesSetter(swspec)), false),
newSingleLineTagParser("Schemes", newSetSchemes(metaSchemeSetter(swspec))),
newMultiLineTagParser("Security", newSetSecurity(rxSecuritySchemes, metaSecuritySetter(swspec)), false),
newMultiLineTagParser("SecurityDefinitions", newYamlParser(rxSecurity, metaSecurityDefinitionsSetter(swspec)), true),
newSingleLineTagParser("Version", &setMetaSingle{swspec, rxVersion, setInfoVersion}),
newSingleLineTagParser("Host", &setMetaSingle{swspec, rxHost, setSwaggerHost}),
newSingleLineTagParser("BasePath", &setMetaSingle{swspec, rxBasePath, setSwaggerBasePath}),
newSingleLineTagParser("Contact", &setMetaSingle{swspec, rxContact, setInfoContact}),
newSingleLineTagParser("License", &setMetaSingle{swspec, rxLicense, setInfoLicense}),
newMultiLineTagParser("YAMLInfoExtensionsBlock", newYamlParser(rxInfoExtensions, infoVendorExtensibleSetter(swspec)), true),
newMultiLineTagParser("YAMLExtensionsBlock", newYamlParser(rxExtensions, metaVendorExtensibleSetter(swspec)), true),
}
return sp
}
type setMetaSingle struct {
spec *spec.Swagger
rx *regexp.Regexp
set func(spec *spec.Swagger, lines []string) error
}
func (s *setMetaSingle) Matches(line string) bool {
return s.rx.MatchString(line)
}
func (s *setMetaSingle) Parse(lines []string) error {
if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) {
return nil
}
matches := s.rx.FindStringSubmatch(lines[0])
if len(matches) > 1 && len(matches[1]) > 0 {
return s.set(s.spec, []string{matches[1]})
}
return nil
}
func setSwaggerHost(swspec *spec.Swagger, lines []string) error {
lns := lines
if len(lns) == 0 || (len(lines) == 1 && len(lines[0]) == 0) {
lns = []string{"localhost"}
}
swspec.Host = lns[0]
return nil
}
func setSwaggerBasePath(swspec *spec.Swagger, lines []string) error {
var ln string
if len(lines) > 0 {
ln = lines[0]
}
swspec.BasePath = ln
return nil
}
func setInfoVersion(swspec *spec.Swagger, lines []string) error {
if len(lines) == 0 {
return nil
}
info := safeInfo(swspec)
info.Version = strings.TrimSpace(lines[0])
return nil
}
func setInfoContact(swspec *spec.Swagger, lines []string) error {
if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) {
return nil
}
contact, err := parseContactInfo(lines[0])
if err != nil {
return err
}
info := safeInfo(swspec)
info.Contact = contact
return nil
}
func parseContactInfo(line string) (*spec.ContactInfo, error) {
nameEmail, url := splitURL(line)
var name, email string
if len(nameEmail) > 0 {
addr, err := mail.ParseAddress(nameEmail)
if err != nil {
return nil, err
}
name, email = addr.Name, addr.Address
}
return &spec.ContactInfo{
ContactInfoProps: spec.ContactInfoProps{
URL: url,
Name: name,
Email: email,
},
}, nil
}
func setInfoLicense(swspec *spec.Swagger, lines []string) error {
if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) {
return nil
}
info := safeInfo(swspec)
line := lines[0]
name, url := splitURL(line)
info.License = &spec.License{
LicenseProps: spec.LicenseProps{
Name: name,
URL: url,
},
}
return nil
}
func safeInfo(swspec *spec.Swagger) *spec.Info {
if swspec.Info == nil {
swspec.Info = new(spec.Info)
}
return swspec.Info
}
// httpFTPScheme matches http://, https://, ws://, wss://
var httpFTPScheme = regexp.MustCompile("(?:(?:ht|f)tp|ws)s?://")
func splitURL(line string) (notURL, url string) {
str := strings.TrimSpace(line)
parts := httpFTPScheme.FindStringIndex(str)
if len(parts) == 0 {
if len(str) > 0 {
notURL = str
}
return
}
if len(parts) > 0 {
notURL = strings.TrimSpace(str[:parts[0]])
url = strings.TrimSpace(str[parts[0]:])
}
return
}

View file

@ -0,0 +1,170 @@
package codescan
import (
"fmt"
"go/ast"
"regexp"
"strings"
"github.com/go-openapi/spec"
)
type operationsBuilder struct {
ctx *scanCtx
path parsedPathContent
operations map[string]*spec.Operation
}
func (o *operationsBuilder) Build(tgt *spec.Paths) error {
pthObj := tgt.Paths[o.path.Path]
op := setPathOperation(
o.path.Method, o.path.ID,
&pthObj, o.operations[o.path.ID])
op.Tags = o.path.Tags
sp := new(yamlSpecScanner)
sp.setTitle = func(lines []string) { op.Summary = joinDropLast(lines) }
sp.setDescription = func(lines []string) { op.Description = joinDropLast(lines) }
if err := sp.Parse(o.path.Remaining); err != nil {
return fmt.Errorf("operation (%s): %v", op.ID, err)
}
if err := sp.UnmarshalSpec(op.UnmarshalJSON); err != nil {
return fmt.Errorf("operation (%s): %v", op.ID, err)
}
if tgt.Paths == nil {
tgt.Paths = make(map[string]spec.PathItem)
}
tgt.Paths[o.path.Path] = pthObj
return nil
}
type parsedPathContent struct {
Method, Path, ID string
Tags []string
Remaining *ast.CommentGroup
}
func parsePathAnnotation(annotation *regexp.Regexp, lines []*ast.Comment) (cnt parsedPathContent) {
var justMatched bool
for _, cmt := range lines {
txt := cmt.Text
for _, line := range strings.Split(txt, "\n") {
matches := annotation.FindStringSubmatch(line)
if len(matches) > 3 {
cnt.Method, cnt.Path, cnt.ID = matches[1], matches[2], matches[len(matches)-1]
cnt.Tags = rxSpace.Split(matches[3], -1)
if len(matches[3]) == 0 {
cnt.Tags = nil
}
justMatched = true
} else if cnt.Method != "" {
if cnt.Remaining == nil {
cnt.Remaining = new(ast.CommentGroup)
}
if !justMatched || strings.TrimSpace(rxStripComments.ReplaceAllString(line, "")) != "" {
cc := new(ast.Comment)
cc.Slash = cmt.Slash
cc.Text = line
cnt.Remaining.List = append(cnt.Remaining.List, cc)
justMatched = false
}
}
}
}
return
}
func setPathOperation(method, id string, pthObj *spec.PathItem, op *spec.Operation) *spec.Operation {
if op == nil {
op = new(spec.Operation)
op.ID = id
}
switch strings.ToUpper(method) {
case "GET":
if pthObj.Get != nil {
if id == pthObj.Get.ID {
op = pthObj.Get
} else {
pthObj.Get = op
}
} else {
pthObj.Get = op
}
case "POST":
if pthObj.Post != nil {
if id == pthObj.Post.ID {
op = pthObj.Post
} else {
pthObj.Post = op
}
} else {
pthObj.Post = op
}
case "PUT":
if pthObj.Put != nil {
if id == pthObj.Put.ID {
op = pthObj.Put
} else {
pthObj.Put = op
}
} else {
pthObj.Put = op
}
case "PATCH":
if pthObj.Patch != nil {
if id == pthObj.Patch.ID {
op = pthObj.Patch
} else {
pthObj.Patch = op
}
} else {
pthObj.Patch = op
}
case "HEAD":
if pthObj.Head != nil {
if id == pthObj.Head.ID {
op = pthObj.Head
} else {
pthObj.Head = op
}
} else {
pthObj.Head = op
}
case "DELETE":
if pthObj.Delete != nil {
if id == pthObj.Delete.ID {
op = pthObj.Delete
} else {
pthObj.Delete = op
}
} else {
pthObj.Delete = op
}
case "OPTIONS":
if pthObj.Options != nil {
if id == pthObj.Options.ID {
op = pthObj.Options
} else {
pthObj.Options = op
}
} else {
pthObj.Options = op
}
}
return op
}

View file

@ -0,0 +1,518 @@
package codescan
import (
"fmt"
"go/ast"
"go/types"
"strings"
"golang.org/x/tools/go/ast/astutil"
"github.com/pkg/errors"
"github.com/go-openapi/spec"
)
type paramTypable struct {
param *spec.Parameter
}
func (pt paramTypable) Level() int { return 0 }
func (pt paramTypable) Typed(tpe, format string) {
pt.param.Typed(tpe, format)
}
func (pt paramTypable) SetRef(ref spec.Ref) {
pt.param.Ref = ref
}
func (pt paramTypable) Items() swaggerTypable {
bdt, schema := bodyTypable(pt.param.In, pt.param.Schema)
if bdt != nil {
pt.param.Schema = schema
return bdt
}
if pt.param.Items == nil {
pt.param.Items = new(spec.Items)
}
pt.param.Type = "array"
return itemsTypable{pt.param.Items, 1}
}
func (pt paramTypable) Schema() *spec.Schema {
if pt.param.In != "body" {
return nil
}
if pt.param.Schema == nil {
pt.param.Schema = new(spec.Schema)
}
return pt.param.Schema
}
func (pt paramTypable) AddExtension(key string, value interface{}) {
if pt.param.In == "body" {
pt.Schema().AddExtension(key, value)
} else {
pt.param.AddExtension(key, value)
}
}
func (pt paramTypable) WithEnum(values ...interface{}) {
pt.param.WithEnum(values...)
}
func (pt paramTypable) WithEnumDescription(desc string) {
if desc == "" {
return
}
pt.param.AddExtension(extEnumDesc, desc)
}
type itemsTypable struct {
items *spec.Items
level int
}
func (pt itemsTypable) Level() int { return pt.level }
func (pt itemsTypable) Typed(tpe, format string) {
pt.items.Typed(tpe, format)
}
func (pt itemsTypable) SetRef(ref spec.Ref) {
pt.items.Ref = ref
}
func (pt itemsTypable) Schema() *spec.Schema {
return nil
}
func (pt itemsTypable) Items() swaggerTypable {
if pt.items.Items == nil {
pt.items.Items = new(spec.Items)
}
pt.items.Type = "array"
return itemsTypable{pt.items.Items, pt.level + 1}
}
func (pt itemsTypable) AddExtension(key string, value interface{}) {
pt.items.AddExtension(key, value)
}
func (pt itemsTypable) WithEnum(values ...interface{}) {
pt.items.WithEnum(values...)
}
func (pt itemsTypable) WithEnumDescription(_ string) {
// no
}
type paramValidations struct {
current *spec.Parameter
}
func (sv paramValidations) SetMaximum(val float64, exclusive bool) {
sv.current.Maximum = &val
sv.current.ExclusiveMaximum = exclusive
}
func (sv paramValidations) SetMinimum(val float64, exclusive bool) {
sv.current.Minimum = &val
sv.current.ExclusiveMinimum = exclusive
}
func (sv paramValidations) SetMultipleOf(val float64) { sv.current.MultipleOf = &val }
func (sv paramValidations) SetMinItems(val int64) { sv.current.MinItems = &val }
func (sv paramValidations) SetMaxItems(val int64) { sv.current.MaxItems = &val }
func (sv paramValidations) SetMinLength(val int64) { sv.current.MinLength = &val }
func (sv paramValidations) SetMaxLength(val int64) { sv.current.MaxLength = &val }
func (sv paramValidations) SetPattern(val string) { sv.current.Pattern = val }
func (sv paramValidations) SetUnique(val bool) { sv.current.UniqueItems = val }
func (sv paramValidations) SetCollectionFormat(val string) { sv.current.CollectionFormat = val }
func (sv paramValidations) SetEnum(val string) {
sv.current.Enum = parseEnum(val, &spec.SimpleSchema{Type: sv.current.Type, Format: sv.current.Format})
}
func (sv paramValidations) SetDefault(val interface{}) { sv.current.Default = val }
func (sv paramValidations) SetExample(val interface{}) { sv.current.Example = val }
type itemsValidations struct {
current *spec.Items
}
func (sv itemsValidations) SetMaximum(val float64, exclusive bool) {
sv.current.Maximum = &val
sv.current.ExclusiveMaximum = exclusive
}
func (sv itemsValidations) SetMinimum(val float64, exclusive bool) {
sv.current.Minimum = &val
sv.current.ExclusiveMinimum = exclusive
}
func (sv itemsValidations) SetMultipleOf(val float64) { sv.current.MultipleOf = &val }
func (sv itemsValidations) SetMinItems(val int64) { sv.current.MinItems = &val }
func (sv itemsValidations) SetMaxItems(val int64) { sv.current.MaxItems = &val }
func (sv itemsValidations) SetMinLength(val int64) { sv.current.MinLength = &val }
func (sv itemsValidations) SetMaxLength(val int64) { sv.current.MaxLength = &val }
func (sv itemsValidations) SetPattern(val string) { sv.current.Pattern = val }
func (sv itemsValidations) SetUnique(val bool) { sv.current.UniqueItems = val }
func (sv itemsValidations) SetCollectionFormat(val string) { sv.current.CollectionFormat = val }
func (sv itemsValidations) SetEnum(val string) {
sv.current.Enum = parseEnum(val, &spec.SimpleSchema{Type: sv.current.Type, Format: sv.current.Format})
}
func (sv itemsValidations) SetDefault(val interface{}) { sv.current.Default = val }
func (sv itemsValidations) SetExample(val interface{}) { sv.current.Example = val }
type parameterBuilder struct {
ctx *scanCtx
decl *entityDecl
postDecls []*entityDecl
}
func (p *parameterBuilder) Build(operations map[string]*spec.Operation) error {
// check if there is a swagger:parameters tag that is followed by one or more words,
// these words are the ids of the operations this parameter struct applies to
// once type name is found convert it to a schema, by looking up the schema in the
// parameters dictionary that got passed into this parse method
for _, opid := range p.decl.OperationIDS() {
operation, ok := operations[opid]
if !ok {
operation = new(spec.Operation)
operations[opid] = operation
operation.ID = opid
}
debugLog("building parameters for: %s", opid)
// analyze struct body for fields etc
// each exported struct field:
// * gets a type mapped to a go primitive
// * perhaps gets a format
// * has to document the validations that apply for the type and the field
// * when the struct field points to a model it becomes a ref: #/definitions/ModelName
// * comments that aren't tags is used as the description
if err := p.buildFromType(p.decl.Type, operation, make(map[string]spec.Parameter)); err != nil {
return err
}
}
return nil
}
func (p *parameterBuilder) buildFromType(otpe types.Type, op *spec.Operation, seen map[string]spec.Parameter) error {
switch tpe := otpe.(type) {
case *types.Pointer:
return p.buildFromType(tpe.Elem(), op, seen)
case *types.Named:
o := tpe.Obj()
switch stpe := o.Type().Underlying().(type) {
case *types.Struct:
debugLog("build from type %s: %T", tpe.Obj().Name(), otpe)
if decl, found := p.ctx.DeclForType(o.Type()); found {
return p.buildFromStruct(decl, stpe, op, seen)
}
return p.buildFromStruct(p.decl, stpe, op, seen)
default:
return errors.Errorf("unhandled type (%T): %s", stpe, o.Type().Underlying().String())
}
default:
return errors.Errorf("unhandled type (%T): %s", otpe, tpe.String())
}
}
func (p *parameterBuilder) buildFromField(fld *types.Var, tpe types.Type, typable swaggerTypable, seen map[string]spec.Parameter) error {
debugLog("build from field %s: %T", fld.Name(), tpe)
switch ftpe := tpe.(type) {
case *types.Basic:
return swaggerSchemaForType(ftpe.Name(), typable)
case *types.Struct:
sb := schemaBuilder{
decl: p.decl,
ctx: p.ctx,
}
if err := sb.buildFromType(tpe, typable); err != nil {
return err
}
p.postDecls = append(p.postDecls, sb.postDecls...)
return nil
case *types.Pointer:
return p.buildFromField(fld, ftpe.Elem(), typable, seen)
case *types.Interface:
sb := schemaBuilder{
decl: p.decl,
ctx: p.ctx,
}
if err := sb.buildFromType(tpe, typable); err != nil {
return err
}
p.postDecls = append(p.postDecls, sb.postDecls...)
return nil
case *types.Array:
return p.buildFromField(fld, ftpe.Elem(), typable.Items(), seen)
case *types.Slice:
return p.buildFromField(fld, ftpe.Elem(), typable.Items(), seen)
case *types.Map:
schema := new(spec.Schema)
typable.Schema().Typed("object", "").AdditionalProperties = &spec.SchemaOrBool{
Schema: schema,
}
sb := schemaBuilder{
decl: p.decl,
ctx: p.ctx,
}
if err := sb.buildFromType(ftpe.Elem(), schemaTypable{schema, typable.Level() + 1}); err != nil {
return err
}
return nil
case *types.Named:
if decl, found := p.ctx.DeclForType(ftpe.Obj().Type()); found {
if decl.Type.Obj().Pkg().Path() == "time" && decl.Type.Obj().Name() == "Time" {
typable.Typed("string", "date-time")
return nil
}
if sfnm, isf := strfmtName(decl.Comments); isf {
typable.Typed("string", sfnm)
return nil
}
sb := &schemaBuilder{ctx: p.ctx, decl: decl}
sb.inferNames()
if err := sb.buildFromType(decl.Type, typable); err != nil {
return err
}
p.postDecls = append(p.postDecls, sb.postDecls...)
return nil
}
return errors.Errorf("unable to find package and source file for: %s", ftpe.String())
default:
return errors.Errorf("unknown type for %s: %T", fld.String(), fld.Type())
}
}
func spExtensionsSetter(ps *spec.Parameter) func(*spec.Extensions) {
return func(exts *spec.Extensions) {
for name, value := range *exts {
addExtension(&ps.VendorExtensible, name, value)
}
}
}
func (p *parameterBuilder) buildFromStruct(decl *entityDecl, tpe *types.Struct, op *spec.Operation, seen map[string]spec.Parameter) error {
if tpe.NumFields() == 0 {
return nil
}
var sequence []string
for i := 0; i < tpe.NumFields(); i++ {
fld := tpe.Field(i)
if fld.Embedded() {
if err := p.buildFromType(fld.Type(), op, seen); err != nil {
return err
}
continue
}
if !fld.Exported() {
debugLog("skipping field %s because it's not exported", fld.Name())
continue
}
tg := tpe.Tag(i)
var afld *ast.Field
ans, _ := astutil.PathEnclosingInterval(decl.File, fld.Pos(), fld.Pos())
for _, an := range ans {
at, valid := an.(*ast.Field)
if !valid {
continue
}
debugLog("field %s: %s(%T) [%q] ==> %s", fld.Name(), fld.Type().String(), fld.Type(), tg, at.Doc.Text())
afld = at
break
}
if afld == nil {
debugLog("can't find source associated with %s for %s", fld.String(), tpe.String())
continue
}
// if the field is annotated with swagger:ignore, ignore it
if ignored(afld.Doc) {
continue
}
name, ignore, _, err := parseJSONTag(afld)
if err != nil {
return err
}
if ignore {
continue
}
in := "query"
// scan for param location first, this changes some behavior down the line
if afld.Doc != nil {
for _, cmt := range afld.Doc.List {
for _, line := range strings.Split(cmt.Text, "\n") {
matches := rxIn.FindStringSubmatch(line)
if len(matches) > 0 && len(strings.TrimSpace(matches[1])) > 0 {
in = strings.TrimSpace(matches[1])
}
}
}
}
ps := seen[name]
ps.In = in
var pty swaggerTypable = paramTypable{&ps}
if in == "body" {
pty = schemaTypable{pty.Schema(), 0}
}
if in == "formData" && afld.Doc != nil && fileParam(afld.Doc) {
pty.Typed("file", "")
} else if err := p.buildFromField(fld, fld.Type(), pty, seen); err != nil {
return err
}
if strfmtName, ok := strfmtName(afld.Doc); ok {
ps.Typed("string", strfmtName)
ps.Ref = spec.Ref{}
ps.Items = nil
}
sp := new(sectionedParser)
sp.setDescription = func(lines []string) {
ps.Description = joinDropLast(lines)
enumDesc := getEnumDesc(ps.Extensions)
if enumDesc != "" {
ps.Description += "\n" + enumDesc
}
}
if ps.Ref.String() == "" {
sp.taggers = []tagParser{
newSingleLineTagParser("in", &matchOnlyParam{&ps, rxIn}),
newSingleLineTagParser("maximum", &setMaximum{paramValidations{&ps}, rxf(rxMaximumFmt, "")}),
newSingleLineTagParser("minimum", &setMinimum{paramValidations{&ps}, rxf(rxMinimumFmt, "")}),
newSingleLineTagParser("multipleOf", &setMultipleOf{paramValidations{&ps}, rxf(rxMultipleOfFmt, "")}),
newSingleLineTagParser("minLength", &setMinLength{paramValidations{&ps}, rxf(rxMinLengthFmt, "")}),
newSingleLineTagParser("maxLength", &setMaxLength{paramValidations{&ps}, rxf(rxMaxLengthFmt, "")}),
newSingleLineTagParser("pattern", &setPattern{paramValidations{&ps}, rxf(rxPatternFmt, "")}),
newSingleLineTagParser("collectionFormat", &setCollectionFormat{paramValidations{&ps}, rxf(rxCollectionFormatFmt, "")}),
newSingleLineTagParser("minItems", &setMinItems{paramValidations{&ps}, rxf(rxMinItemsFmt, "")}),
newSingleLineTagParser("maxItems", &setMaxItems{paramValidations{&ps}, rxf(rxMaxItemsFmt, "")}),
newSingleLineTagParser("unique", &setUnique{paramValidations{&ps}, rxf(rxUniqueFmt, "")}),
newSingleLineTagParser("enum", &setEnum{paramValidations{&ps}, rxf(rxEnumFmt, "")}),
newSingleLineTagParser("default", &setDefault{&ps.SimpleSchema, paramValidations{&ps}, rxf(rxDefaultFmt, "")}),
newSingleLineTagParser("example", &setExample{&ps.SimpleSchema, paramValidations{&ps}, rxf(rxExampleFmt, "")}),
newSingleLineTagParser("required", &setRequiredParam{&ps}),
newMultiLineTagParser("Extensions", newSetExtensions(spExtensionsSetter(&ps)), true),
}
itemsTaggers := func(items *spec.Items, level int) []tagParser {
// the expression is 1-index based not 0-index
itemsPrefix := fmt.Sprintf(rxItemsPrefixFmt, level+1)
return []tagParser{
newSingleLineTagParser(fmt.Sprintf("items%dMaximum", level), &setMaximum{itemsValidations{items}, rxf(rxMaximumFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dMinimum", level), &setMinimum{itemsValidations{items}, rxf(rxMinimumFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dMultipleOf", level), &setMultipleOf{itemsValidations{items}, rxf(rxMultipleOfFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dMinLength", level), &setMinLength{itemsValidations{items}, rxf(rxMinLengthFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dMaxLength", level), &setMaxLength{itemsValidations{items}, rxf(rxMaxLengthFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dPattern", level), &setPattern{itemsValidations{items}, rxf(rxPatternFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dCollectionFormat", level), &setCollectionFormat{itemsValidations{items}, rxf(rxCollectionFormatFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dMinItems", level), &setMinItems{itemsValidations{items}, rxf(rxMinItemsFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dMaxItems", level), &setMaxItems{itemsValidations{items}, rxf(rxMaxItemsFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dUnique", level), &setUnique{itemsValidations{items}, rxf(rxUniqueFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dEnum", level), &setEnum{itemsValidations{items}, rxf(rxEnumFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dDefault", level), &setDefault{&items.SimpleSchema, itemsValidations{items}, rxf(rxDefaultFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dExample", level), &setExample{&items.SimpleSchema, itemsValidations{items}, rxf(rxExampleFmt, itemsPrefix)}),
}
}
var parseArrayTypes func(expr ast.Expr, items *spec.Items, level int) ([]tagParser, error)
parseArrayTypes = func(expr ast.Expr, items *spec.Items, level int) ([]tagParser, error) {
if items == nil {
return []tagParser{}, nil
}
switch iftpe := expr.(type) {
case *ast.ArrayType:
eleTaggers := itemsTaggers(items, level)
sp.taggers = append(eleTaggers, sp.taggers...)
otherTaggers, err := parseArrayTypes(iftpe.Elt, items.Items, level+1)
if err != nil {
return nil, err
}
return otherTaggers, nil
case *ast.SelectorExpr:
otherTaggers, err := parseArrayTypes(iftpe.Sel, items.Items, level+1)
if err != nil {
return nil, err
}
return otherTaggers, nil
case *ast.Ident:
taggers := []tagParser{}
if iftpe.Obj == nil {
taggers = itemsTaggers(items, level)
}
otherTaggers, err := parseArrayTypes(expr, items.Items, level+1)
if err != nil {
return nil, err
}
return append(taggers, otherTaggers...), nil
case *ast.StarExpr:
otherTaggers, err := parseArrayTypes(iftpe.X, items, level)
if err != nil {
return nil, err
}
return otherTaggers, nil
default:
return nil, fmt.Errorf("unknown field type ele for %q", name)
}
}
// check if this is a primitive, if so parse the validations from the
// doc comments of the slice declaration.
if ftped, ok := afld.Type.(*ast.ArrayType); ok {
taggers, err := parseArrayTypes(ftped.Elt, ps.Items, 0)
if err != nil {
return err
}
sp.taggers = append(taggers, sp.taggers...)
}
} else {
sp.taggers = []tagParser{
newSingleLineTagParser("in", &matchOnlyParam{&ps, rxIn}),
newSingleLineTagParser("required", &matchOnlyParam{&ps, rxRequired}),
newMultiLineTagParser("Extensions", newSetExtensions(spExtensionsSetter(&ps)), true),
}
}
if err := sp.Parse(afld.Doc); err != nil {
return err
}
if ps.In == "path" {
ps.Required = true
}
if ps.Name == "" {
ps.Name = name
}
if name != fld.Name() {
addExtension(&ps.VendorExtensible, "x-go-name", fld.Name())
}
seen[name] = ps
sequence = append(sequence, name)
}
for _, k := range sequence {
p := seen[k]
for i, v := range op.Parameters {
if v.Name == k {
op.Parameters = append(op.Parameters[:i], op.Parameters[i+1:]...)
break
}
}
op.Parameters = append(op.Parameters, p)
}
return nil
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,51 @@
//go:build go1.19
// +build go1.19
package codescan
import (
"strings"
)
// a shared function that can be used to split given headers
// into a title and description
func collectScannerTitleDescription(headers []string) (title, desc []string) {
hdrs := cleanupScannerLines(headers, rxUncommentHeaders, nil)
idx := -1
for i, line := range hdrs {
if strings.TrimSpace(line) == "" {
idx = i
break
}
}
if idx > -1 {
title = hdrs[:idx]
if len(title) > 0 {
title[0] = rxTitleStart.ReplaceAllString(title[0], "")
}
if len(hdrs) > idx+1 {
desc = hdrs[idx+1:]
} else {
desc = nil
}
return
}
if len(hdrs) > 0 {
line := hdrs[0]
switch {
case rxPunctuationEnd.MatchString(line):
title = []string{line}
desc = hdrs[1:]
case rxTitleStart.MatchString(line):
title = []string{rxTitleStart.ReplaceAllString(line, "")}
desc = hdrs[1:]
default:
desc = hdrs
}
}
return
}

View file

@ -0,0 +1,42 @@
//go:build !go1.19
// +build !go1.19
package codescan
import "strings"
// a shared function that can be used to split given headers
// into a title and description
func collectScannerTitleDescription(headers []string) (title, desc []string) {
hdrs := cleanupScannerLines(headers, rxUncommentHeaders, nil)
idx := -1
for i, line := range hdrs {
if strings.TrimSpace(line) == "" {
idx = i
break
}
}
if idx > -1 {
title = hdrs[:idx]
if len(hdrs) > idx+1 {
desc = hdrs[idx+1:]
} else {
desc = nil
}
return
}
if len(hdrs) > 0 {
line := hdrs[0]
if rxPunctuationEnd.MatchString(line) {
title = []string{line}
desc = hdrs[1:]
} else {
desc = hdrs
}
}
return
}

View file

@ -0,0 +1,96 @@
package codescan
import "regexp"
const (
rxMethod = "(\\p{L}+)"
rxPath = "((?:/[\\p{L}\\p{N}\\p{Pd}\\p{Pc}{}\\-\\.\\?_~%!$&'()*+,;=:@/]*)+/?)"
rxOpTags = "(\\p{L}[\\p{L}\\p{N}\\p{Pd}\\.\\p{Pc}\\p{Zs}]+)"
rxOpID = "((?:\\p{L}[\\p{L}\\p{N}\\p{Pd}\\p{Pc}]+)+)"
rxMaximumFmt = "%s[Mm]ax(?:imum)?\\p{Zs}*:\\p{Zs}*([\\<=])?\\p{Zs}*([\\+-]?(?:\\p{N}+\\.)?\\p{N}+)$"
rxMinimumFmt = "%s[Mm]in(?:imum)?\\p{Zs}*:\\p{Zs}*([\\>=])?\\p{Zs}*([\\+-]?(?:\\p{N}+\\.)?\\p{N}+)$"
rxMultipleOfFmt = "%s[Mm]ultiple\\p{Zs}*[Oo]f\\p{Zs}*:\\p{Zs}*([\\+-]?(?:\\p{N}+\\.)?\\p{N}+)$"
rxMaxLengthFmt = "%s[Mm]ax(?:imum)?(?:\\p{Zs}*[\\p{Pd}\\p{Pc}]?[Ll]en(?:gth)?)\\p{Zs}*:\\p{Zs}*(\\p{N}+)$"
rxMinLengthFmt = "%s[Mm]in(?:imum)?(?:\\p{Zs}*[\\p{Pd}\\p{Pc}]?[Ll]en(?:gth)?)\\p{Zs}*:\\p{Zs}*(\\p{N}+)$"
rxPatternFmt = "%s[Pp]attern\\p{Zs}*:\\p{Zs}*(.*)$"
rxCollectionFormatFmt = "%s[Cc]ollection(?:\\p{Zs}*[\\p{Pd}\\p{Pc}]?[Ff]ormat)\\p{Zs}*:\\p{Zs}*(.*)$"
rxEnumFmt = "%s[Ee]num\\p{Zs}*:\\p{Zs}*(.*)$"
rxDefaultFmt = "%s[Dd]efault\\p{Zs}*:\\p{Zs}*(.*)$"
rxExampleFmt = "%s[Ee]xample\\p{Zs}*:\\p{Zs}*(.*)$"
rxMaxItemsFmt = "%s[Mm]ax(?:imum)?(?:\\p{Zs}*|[\\p{Pd}\\p{Pc}]|\\.)?[Ii]tems\\p{Zs}*:\\p{Zs}*(\\p{N}+)$"
rxMinItemsFmt = "%s[Mm]in(?:imum)?(?:\\p{Zs}*|[\\p{Pd}\\p{Pc}]|\\.)?[Ii]tems\\p{Zs}*:\\p{Zs}*(\\p{N}+)$"
rxUniqueFmt = "%s[Uu]nique\\p{Zs}*:\\p{Zs}*(true|false)$"
rxItemsPrefixFmt = "(?:[Ii]tems[\\.\\p{Zs}]*){%d}"
)
var (
rxSwaggerAnnotation = regexp.MustCompile(`swagger:([\p{L}\p{N}\p{Pd}\p{Pc}]+)`)
rxFileUpload = regexp.MustCompile(`swagger:file`)
rxStrFmt = regexp.MustCompile(`swagger:strfmt\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}]+)$`)
rxAlias = regexp.MustCompile(`swagger:alias`)
rxName = regexp.MustCompile(`swagger:name\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}\.]+)$`)
rxAllOf = regexp.MustCompile(`swagger:allOf\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}\.]+)?$`)
rxModelOverride = regexp.MustCompile(`swagger:model\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}]+)?$`)
rxResponseOverride = regexp.MustCompile(`swagger:response\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}]+)?$`)
rxParametersOverride = regexp.MustCompile(`swagger:parameters\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}\p{Zs}]+)$`)
rxEnum = regexp.MustCompile(`swagger:enum\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}]+)$`)
rxIgnoreOverride = regexp.MustCompile(`swagger:ignore\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}]+)?$`)
rxDefault = regexp.MustCompile(`swagger:default\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}]+)$`)
rxType = regexp.MustCompile(`swagger:type\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}]+)$`)
rxRoute = regexp.MustCompile(
"swagger:route\\p{Zs}*" +
rxMethod +
"\\p{Zs}*" +
rxPath +
"(?:\\p{Zs}+" +
rxOpTags +
")?\\p{Zs}+" +
rxOpID + "\\p{Zs}*$")
rxBeginYAMLSpec = regexp.MustCompile(`---\p{Zs}*$`)
rxUncommentHeaders = regexp.MustCompile(`^[\p{Zs}\t/\*-]*\|?`)
rxUncommentYAML = regexp.MustCompile(`^[\p{Zs}\t]*/*`)
rxOperation = regexp.MustCompile(
"swagger:operation\\p{Zs}*" +
rxMethod +
"\\p{Zs}*" +
rxPath +
"(?:\\p{Zs}+" +
rxOpTags +
")?\\p{Zs}+" +
rxOpID + "\\p{Zs}*$")
rxSpace = regexp.MustCompile(`\p{Zs}+`)
rxIndent = regexp.MustCompile(`[\p{Zs}\t]*/*[\p{Zs}\t]*[^\p{Zs}\t]`)
rxNotIndent = regexp.MustCompile(`[^\p{Zs}\t]`)
rxPunctuationEnd = regexp.MustCompile(`\p{Po}$`)
rxTitleStart = regexp.MustCompile(`^[#]+\p{Zs}+`)
rxStripComments = regexp.MustCompile(`^[^\p{L}\p{N}\p{Pd}\p{Pc}\+]*`)
rxStripTitleComments = regexp.MustCompile(`^[^\p{L}]*[Pp]ackage\p{Zs}+[^\p{Zs}]+\p{Zs}*`)
rxAllowedExtensions = regexp.MustCompile(`^[Xx]-`)
rxIn = regexp.MustCompile(`[Ii]n\p{Zs}*:\p{Zs}*(query|path|header|body|formData)$`)
rxRequired = regexp.MustCompile(`[Rr]equired\p{Zs}*:\p{Zs}*(true|false)$`)
rxDiscriminator = regexp.MustCompile(`[Dd]iscriminator\p{Zs}*:\p{Zs}*(true|false)$`)
rxReadOnly = regexp.MustCompile(`[Rr]ead(?:\p{Zs}*|[\p{Pd}\p{Pc}])?[Oo]nly\p{Zs}*:\p{Zs}*(true|false)$`)
rxConsumes = regexp.MustCompile(`[Cc]onsumes\p{Zs}*:`)
rxProduces = regexp.MustCompile(`[Pp]roduces\p{Zs}*:`)
rxSecuritySchemes = regexp.MustCompile(`[Ss]ecurity\p{Zs}*:`)
rxSecurity = regexp.MustCompile(`[Ss]ecurity\p{Zs}*[Dd]efinitions:`)
rxResponses = regexp.MustCompile(`[Rr]esponses\p{Zs}*:`)
rxParameters = regexp.MustCompile(`[Pp]arameters\p{Zs}*:`)
rxSchemes = regexp.MustCompile(`[Ss]chemes\p{Zs}*:\p{Zs}*((?:(?:https?|HTTPS?|wss?|WSS?)[\p{Zs},]*)+)$`)
rxVersion = regexp.MustCompile(`[Vv]ersion\p{Zs}*:\p{Zs}*(.+)$`)
rxHost = regexp.MustCompile(`[Hh]ost\p{Zs}*:\p{Zs}*(.+)$`)
rxBasePath = regexp.MustCompile(`[Bb]ase\p{Zs}*-*[Pp]ath\p{Zs}*:\p{Zs}*` + rxPath + "$")
rxLicense = regexp.MustCompile(`[Ll]icense\p{Zs}*:\p{Zs}*(.+)$`)
rxContact = regexp.MustCompile(`[Cc]ontact\p{Zs}*-?(?:[Ii]info\p{Zs}*)?:\p{Zs}*(.+)$`)
rxTOS = regexp.MustCompile(`[Tt](:?erms)?\p{Zs}*-?[Oo]f?\p{Zs}*-?[Ss](?:ervice)?\p{Zs}*:`)
rxExtensions = regexp.MustCompile(`[Ee]xtensions\p{Zs}*:`)
rxInfoExtensions = regexp.MustCompile(`[In]nfo\p{Zs}*[Ee]xtensions:`)
rxDeprecated = regexp.MustCompile(`[Dd]eprecated\p{Zs}*:\p{Zs}*(true|false)$`)
// currently unused: rxExample = regexp.MustCompile(`[Ex]ample\p{Zs}*:\p{Zs}*(.*)$`)
)

View file

@ -0,0 +1,454 @@
package codescan
import (
"fmt"
"go/ast"
"go/types"
"strings"
"github.com/pkg/errors"
"golang.org/x/tools/go/ast/astutil"
"github.com/go-openapi/spec"
)
type responseTypable struct {
in string
header *spec.Header
response *spec.Response
}
func (ht responseTypable) Level() int { return 0 }
func (ht responseTypable) Typed(tpe, format string) {
ht.header.Typed(tpe, format)
}
func bodyTypable(in string, schema *spec.Schema) (swaggerTypable, *spec.Schema) {
if in == "body" {
// get the schema for items on the schema property
if schema == nil {
schema = new(spec.Schema)
}
if schema.Items == nil {
schema.Items = new(spec.SchemaOrArray)
}
if schema.Items.Schema == nil {
schema.Items.Schema = new(spec.Schema)
}
schema.Typed("array", "")
return schemaTypable{schema.Items.Schema, 1}, schema
}
return nil, nil
}
func (ht responseTypable) Items() swaggerTypable {
bdt, schema := bodyTypable(ht.in, ht.response.Schema)
if bdt != nil {
ht.response.Schema = schema
return bdt
}
if ht.header.Items == nil {
ht.header.Items = new(spec.Items)
}
ht.header.Type = "array"
return itemsTypable{ht.header.Items, 1}
}
func (ht responseTypable) SetRef(ref spec.Ref) {
// having trouble seeing the usefulness of this one here
ht.Schema().Ref = ref
}
func (ht responseTypable) Schema() *spec.Schema {
if ht.response.Schema == nil {
ht.response.Schema = new(spec.Schema)
}
return ht.response.Schema
}
func (ht responseTypable) SetSchema(schema *spec.Schema) {
ht.response.Schema = schema
}
func (ht responseTypable) CollectionOf(items *spec.Items, format string) {
ht.header.CollectionOf(items, format)
}
func (ht responseTypable) AddExtension(key string, value interface{}) {
ht.response.AddExtension(key, value)
}
func (ht responseTypable) WithEnum(values ...interface{}) {
ht.header.WithEnum(values)
}
func (ht responseTypable) WithEnumDescription(_ string) {
// no
}
type headerValidations struct {
current *spec.Header
}
func (sv headerValidations) SetMaximum(val float64, exclusive bool) {
sv.current.Maximum = &val
sv.current.ExclusiveMaximum = exclusive
}
func (sv headerValidations) SetMinimum(val float64, exclusive bool) {
sv.current.Minimum = &val
sv.current.ExclusiveMinimum = exclusive
}
func (sv headerValidations) SetMultipleOf(val float64) { sv.current.MultipleOf = &val }
func (sv headerValidations) SetMinItems(val int64) { sv.current.MinItems = &val }
func (sv headerValidations) SetMaxItems(val int64) { sv.current.MaxItems = &val }
func (sv headerValidations) SetMinLength(val int64) { sv.current.MinLength = &val }
func (sv headerValidations) SetMaxLength(val int64) { sv.current.MaxLength = &val }
func (sv headerValidations) SetPattern(val string) { sv.current.Pattern = val }
func (sv headerValidations) SetUnique(val bool) { sv.current.UniqueItems = val }
func (sv headerValidations) SetCollectionFormat(val string) { sv.current.CollectionFormat = val }
func (sv headerValidations) SetEnum(val string) {
sv.current.Enum = parseEnum(val, &spec.SimpleSchema{Type: sv.current.Type, Format: sv.current.Format})
}
func (sv headerValidations) SetDefault(val interface{}) { sv.current.Default = val }
func (sv headerValidations) SetExample(val interface{}) { sv.current.Example = val }
type responseBuilder struct {
ctx *scanCtx
decl *entityDecl
postDecls []*entityDecl
}
func (r *responseBuilder) Build(responses map[string]spec.Response) error {
// check if there is a swagger:response tag that is followed by one or more words,
// these words are the ids of the operations this parameter struct applies to
// once type name is found convert it to a schema, by looking up the schema in the
// parameters dictionary that got passed into this parse method
name, _ := r.decl.ResponseNames()
response := responses[name]
debugLog("building response: %s", name)
// analyze doc comment for the model
sp := new(sectionedParser)
sp.setDescription = func(lines []string) { response.Description = joinDropLast(lines) }
if err := sp.Parse(r.decl.Comments); err != nil {
return err
}
// analyze struct body for fields etc
// each exported struct field:
// * gets a type mapped to a go primitive
// * perhaps gets a format
// * has to document the validations that apply for the type and the field
// * when the struct field points to a model it becomes a ref: #/definitions/ModelName
// * comments that aren't tags is used as the description
if err := r.buildFromType(r.decl.Type, &response, make(map[string]bool)); err != nil {
return err
}
responses[name] = response
return nil
}
func (r *responseBuilder) buildFromField(fld *types.Var, tpe types.Type, typable swaggerTypable, seen map[string]bool) error {
debugLog("build from field %s: %T", fld.Name(), tpe)
switch ftpe := tpe.(type) {
case *types.Basic:
return swaggerSchemaForType(ftpe.Name(), typable)
case *types.Struct:
sb := schemaBuilder{
decl: r.decl,
ctx: r.ctx,
}
if err := sb.buildFromType(tpe, typable); err != nil {
return err
}
r.postDecls = append(r.postDecls, sb.postDecls...)
return nil
case *types.Pointer:
return r.buildFromField(fld, ftpe.Elem(), typable, seen)
case *types.Interface:
sb := schemaBuilder{
decl: r.decl,
ctx: r.ctx,
}
if err := sb.buildFromType(tpe, typable); err != nil {
return err
}
r.postDecls = append(r.postDecls, sb.postDecls...)
return nil
case *types.Array:
return r.buildFromField(fld, ftpe.Elem(), typable.Items(), seen)
case *types.Slice:
return r.buildFromField(fld, ftpe.Elem(), typable.Items(), seen)
case *types.Map:
schema := new(spec.Schema)
typable.Schema().Typed("object", "").AdditionalProperties = &spec.SchemaOrBool{
Schema: schema,
}
sb := schemaBuilder{
decl: r.decl,
ctx: r.ctx,
}
if err := sb.buildFromType(ftpe.Elem(), schemaTypable{schema, typable.Level() + 1}); err != nil {
return err
}
r.postDecls = append(r.postDecls, sb.postDecls...)
return nil
case *types.Named:
if decl, found := r.ctx.DeclForType(ftpe.Obj().Type()); found {
if decl.Type.Obj().Pkg().Path() == "time" && decl.Type.Obj().Name() == "Time" {
typable.Typed("string", "date-time")
return nil
}
if sfnm, isf := strfmtName(decl.Comments); isf {
typable.Typed("string", sfnm)
return nil
}
sb := &schemaBuilder{ctx: r.ctx, decl: decl}
sb.inferNames()
if err := sb.buildFromType(decl.Type, typable); err != nil {
return err
}
r.postDecls = append(r.postDecls, sb.postDecls...)
return nil
}
return errors.Errorf("unable to find package and source file for: %s", ftpe.String())
default:
return errors.Errorf("unknown type for %s: %T", fld.String(), fld.Type())
}
}
func (r *responseBuilder) buildFromType(otpe types.Type, resp *spec.Response, seen map[string]bool) error {
switch tpe := otpe.(type) {
case *types.Pointer:
return r.buildFromType(tpe.Elem(), resp, seen)
case *types.Named:
o := tpe.Obj()
switch stpe := o.Type().Underlying().(type) {
case *types.Struct:
debugLog("build from type %s: %T", tpe.Obj().Name(), otpe)
if decl, found := r.ctx.DeclForType(o.Type()); found {
return r.buildFromStruct(decl, stpe, resp, seen)
}
return r.buildFromStruct(r.decl, stpe, resp, seen)
default:
if decl, found := r.ctx.DeclForType(o.Type()); found {
var schema spec.Schema
typable := schemaTypable{schema: &schema, level: 0}
if decl.Type.Obj().Pkg().Path() == "time" && decl.Type.Obj().Name() == "Time" {
typable.Typed("string", "date-time")
return nil
}
if sfnm, isf := strfmtName(decl.Comments); isf {
typable.Typed("string", sfnm)
return nil
}
sb := &schemaBuilder{ctx: r.ctx, decl: decl}
sb.inferNames()
if err := sb.buildFromType(tpe.Underlying(), typable); err != nil {
return err
}
resp.WithSchema(&schema)
r.postDecls = append(r.postDecls, sb.postDecls...)
return nil
}
return errors.Errorf("responses can only be structs, did you mean for %s to be the response body?", otpe.String())
}
default:
return errors.New("anonymous types are currently not supported for responses")
}
}
func (r *responseBuilder) buildFromStruct(decl *entityDecl, tpe *types.Struct, resp *spec.Response, seen map[string]bool) error {
if tpe.NumFields() == 0 {
return nil
}
for i := 0; i < tpe.NumFields(); i++ {
fld := tpe.Field(i)
if fld.Embedded() {
if err := r.buildFromType(fld.Type(), resp, seen); err != nil {
return err
}
continue
}
if fld.Anonymous() {
debugLog("skipping anonymous field")
continue
}
tg := tpe.Tag(i)
var afld *ast.Field
ans, _ := astutil.PathEnclosingInterval(decl.File, fld.Pos(), fld.Pos())
for _, an := range ans {
at, valid := an.(*ast.Field)
if !valid {
continue
}
debugLog("field %s: %s(%T) [%q] ==> %s", fld.Name(), fld.Type().String(), fld.Type(), tg, at.Doc.Text())
afld = at
break
}
if afld == nil {
debugLog("can't find source associated with %s for %s", fld.String(), tpe.String())
continue
}
// if the field is annotated with swagger:ignore, ignore it
if ignored(afld.Doc) {
continue
}
name, ignore, _, err := parseJSONTag(afld)
if err != nil {
return err
}
if ignore {
continue
}
var in string
// scan for param location first, this changes some behavior down the line
if afld.Doc != nil {
for _, cmt := range afld.Doc.List {
for _, line := range strings.Split(cmt.Text, "\n") {
matches := rxIn.FindStringSubmatch(line)
if len(matches) > 0 && len(strings.TrimSpace(matches[1])) > 0 {
in = strings.TrimSpace(matches[1])
}
}
}
}
ps := resp.Headers[name]
// support swagger:file for response
// An API operation can return a file, such as an image or PDF. In this case,
// define the response schema with type: file and specify the appropriate MIME types in the produces section.
if afld.Doc != nil && fileParam(afld.Doc) {
resp.Schema = &spec.Schema{}
resp.Schema.Typed("file", "")
} else if err := r.buildFromField(fld, fld.Type(), responseTypable{in, &ps, resp}, seen); err != nil {
return err
}
if strfmtName, ok := strfmtName(afld.Doc); ok {
ps.Typed("string", strfmtName)
}
sp := new(sectionedParser)
sp.setDescription = func(lines []string) { ps.Description = joinDropLast(lines) }
sp.taggers = []tagParser{
newSingleLineTagParser("maximum", &setMaximum{headerValidations{&ps}, rxf(rxMaximumFmt, "")}),
newSingleLineTagParser("minimum", &setMinimum{headerValidations{&ps}, rxf(rxMinimumFmt, "")}),
newSingleLineTagParser("multipleOf", &setMultipleOf{headerValidations{&ps}, rxf(rxMultipleOfFmt, "")}),
newSingleLineTagParser("minLength", &setMinLength{headerValidations{&ps}, rxf(rxMinLengthFmt, "")}),
newSingleLineTagParser("maxLength", &setMaxLength{headerValidations{&ps}, rxf(rxMaxLengthFmt, "")}),
newSingleLineTagParser("pattern", &setPattern{headerValidations{&ps}, rxf(rxPatternFmt, "")}),
newSingleLineTagParser("collectionFormat", &setCollectionFormat{headerValidations{&ps}, rxf(rxCollectionFormatFmt, "")}),
newSingleLineTagParser("minItems", &setMinItems{headerValidations{&ps}, rxf(rxMinItemsFmt, "")}),
newSingleLineTagParser("maxItems", &setMaxItems{headerValidations{&ps}, rxf(rxMaxItemsFmt, "")}),
newSingleLineTagParser("unique", &setUnique{headerValidations{&ps}, rxf(rxUniqueFmt, "")}),
newSingleLineTagParser("enum", &setEnum{headerValidations{&ps}, rxf(rxEnumFmt, "")}),
newSingleLineTagParser("default", &setDefault{&ps.SimpleSchema, headerValidations{&ps}, rxf(rxDefaultFmt, "")}),
newSingleLineTagParser("example", &setExample{&ps.SimpleSchema, headerValidations{&ps}, rxf(rxExampleFmt, "")}),
}
itemsTaggers := func(items *spec.Items, level int) []tagParser {
// the expression is 1-index based not 0-index
itemsPrefix := fmt.Sprintf(rxItemsPrefixFmt, level+1)
return []tagParser{
newSingleLineTagParser(fmt.Sprintf("items%dMaximum", level), &setMaximum{itemsValidations{items}, rxf(rxMaximumFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dMinimum", level), &setMinimum{itemsValidations{items}, rxf(rxMinimumFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dMultipleOf", level), &setMultipleOf{itemsValidations{items}, rxf(rxMultipleOfFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dMinLength", level), &setMinLength{itemsValidations{items}, rxf(rxMinLengthFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dMaxLength", level), &setMaxLength{itemsValidations{items}, rxf(rxMaxLengthFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dPattern", level), &setPattern{itemsValidations{items}, rxf(rxPatternFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dCollectionFormat", level), &setCollectionFormat{itemsValidations{items}, rxf(rxCollectionFormatFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dMinItems", level), &setMinItems{itemsValidations{items}, rxf(rxMinItemsFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dMaxItems", level), &setMaxItems{itemsValidations{items}, rxf(rxMaxItemsFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dUnique", level), &setUnique{itemsValidations{items}, rxf(rxUniqueFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dEnum", level), &setEnum{itemsValidations{items}, rxf(rxEnumFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dDefault", level), &setDefault{&items.SimpleSchema, itemsValidations{items}, rxf(rxDefaultFmt, itemsPrefix)}),
newSingleLineTagParser(fmt.Sprintf("items%dExample", level), &setExample{&items.SimpleSchema, itemsValidations{items}, rxf(rxExampleFmt, itemsPrefix)}),
}
}
var parseArrayTypes func(expr ast.Expr, items *spec.Items, level int) ([]tagParser, error)
parseArrayTypes = func(expr ast.Expr, items *spec.Items, level int) ([]tagParser, error) {
if items == nil {
return []tagParser{}, nil
}
switch iftpe := expr.(type) {
case *ast.ArrayType:
eleTaggers := itemsTaggers(items, level)
sp.taggers = append(eleTaggers, sp.taggers...)
otherTaggers, err := parseArrayTypes(iftpe.Elt, items.Items, level+1)
if err != nil {
return nil, err
}
return otherTaggers, nil
case *ast.Ident:
taggers := []tagParser{}
if iftpe.Obj == nil {
taggers = itemsTaggers(items, level)
}
otherTaggers, err := parseArrayTypes(expr, items.Items, level+1)
if err != nil {
return nil, err
}
return append(taggers, otherTaggers...), nil
case *ast.SelectorExpr:
otherTaggers, err := parseArrayTypes(iftpe.Sel, items.Items, level+1)
if err != nil {
return nil, err
}
return otherTaggers, nil
case *ast.StarExpr:
otherTaggers, err := parseArrayTypes(iftpe.X, items, level)
if err != nil {
return nil, err
}
return otherTaggers, nil
default:
return nil, fmt.Errorf("unknown field type ele for %q", name)
}
}
// check if this is a primitive, if so parse the validations from the
// doc comments of the slice declaration.
if ftped, ok := afld.Type.(*ast.ArrayType); ok {
taggers, err := parseArrayTypes(ftped.Elt, ps.Items, 0)
if err != nil {
return err
}
sp.taggers = append(taggers, sp.taggers...)
}
if err := sp.Parse(afld.Doc); err != nil {
return err
}
if in != "body" {
seen[name] = true
if resp.Headers == nil {
resp.Headers = make(map[string]spec.Header)
}
resp.Headers[name] = ps
}
}
for k := range resp.Headers {
if !seen[k] {
delete(resp.Headers, k)
}
}
return nil
}

View file

@ -0,0 +1,263 @@
package codescan
import (
"errors"
"strconv"
"strings"
"github.com/go-openapi/spec"
)
const (
// ParamDescriptionKey indicates the tag used to define a parameter description in swagger:route
ParamDescriptionKey = "description"
// ParamNameKey indicates the tag used to define a parameter name in swagger:route
ParamNameKey = "name"
// ParamInKey indicates the tag used to define a parameter location in swagger:route
ParamInKey = "in"
// ParamRequiredKey indicates the tag used to declare whether a parameter is required in swagger:route
ParamRequiredKey = "required"
// ParamTypeKey indicates the tag used to define the parameter type in swagger:route
ParamTypeKey = "type"
// ParamAllowEmptyKey indicates the tag used to indicate whether a parameter allows empty values in swagger:route
ParamAllowEmptyKey = "allowempty"
// SchemaMinKey indicates the tag used to indicate the minimum value allowed for this type in swagger:route
SchemaMinKey = "min"
// SchemaMaxKey indicates the tag used to indicate the maximum value allowed for this type in swagger:route
SchemaMaxKey = "max"
// SchemaEnumKey indicates the tag used to specify the allowed values for this type in swagger:route
SchemaEnumKey = "enum"
// SchemaFormatKey indicates the expected format for this field in swagger:route
SchemaFormatKey = "format"
// SchemaDefaultKey indicates the default value for this field in swagger:route
SchemaDefaultKey = "default"
// SchemaMinLenKey indicates the minimum length this field in swagger:route
SchemaMinLenKey = "minlength"
// SchemaMaxLenKey indicates the minimum length this field in swagger:route
SchemaMaxLenKey = "maxlength"
// TypeArray is the identifier for an array type in swagger:route
TypeArray = "array"
// TypeNumber is the identifier for a number type in swagger:route
TypeNumber = "number"
// TypeInteger is the identifier for an integer type in swagger:route
TypeInteger = "integer"
// TypeBoolean is the identifier for a boolean type in swagger:route
TypeBoolean = "boolean"
// TypeBool is the identifier for a boolean type in swagger:route
TypeBool = "bool"
// TypeObject is the identifier for an object type in swagger:route
TypeObject = "object"
// TypeString is the identifier for a string type in swagger:route
TypeString = "string"
)
var (
validIn = []string{"path", "query", "header", "body", "form"}
basicTypes = []string{TypeInteger, TypeNumber, TypeString, TypeBoolean, TypeBool, TypeArray}
)
func newSetParams(params []*spec.Parameter, setter func([]*spec.Parameter)) *setOpParams {
return &setOpParams{
set: setter,
parameters: params,
}
}
type setOpParams struct {
set func([]*spec.Parameter)
parameters []*spec.Parameter
}
func (s *setOpParams) Matches(line string) bool {
return rxParameters.MatchString(line)
}
func (s *setOpParams) Parse(lines []string) error {
if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) {
return nil
}
var current *spec.Parameter
var extraData map[string]string
for _, line := range lines {
l := strings.TrimSpace(line)
if strings.HasPrefix(l, "+") {
s.finalizeParam(current, extraData)
current = new(spec.Parameter)
extraData = make(map[string]string)
l = strings.TrimPrefix(l, "+")
}
kv := strings.SplitN(l, ":", 2)
if len(kv) <= 1 {
continue
}
key := strings.ToLower(strings.TrimSpace(kv[0]))
value := strings.TrimSpace(kv[1])
if current == nil {
return errors.New("invalid route/operation schema provided")
}
switch key {
case ParamDescriptionKey:
current.Description = value
case ParamNameKey:
current.Name = value
case ParamInKey:
v := strings.ToLower(value)
if contains(validIn, v) {
current.In = v
}
case ParamRequiredKey:
if v, err := strconv.ParseBool(value); err == nil {
current.Required = v
}
case ParamTypeKey:
if current.Schema == nil {
current.Schema = new(spec.Schema)
}
if contains(basicTypes, value) {
current.Type = strings.ToLower(value)
if current.Type == TypeBool {
current.Type = TypeBoolean
}
} else if ref, err := spec.NewRef("#/definitions/" + value); err == nil {
current.Type = TypeObject
current.Schema.Ref = ref
}
current.Schema.Type = spec.StringOrArray{current.Type}
case ParamAllowEmptyKey:
if v, err := strconv.ParseBool(value); err == nil {
current.AllowEmptyValue = v
}
default:
extraData[key] = value
}
}
s.finalizeParam(current, extraData)
s.set(s.parameters)
return nil
}
func (s *setOpParams) finalizeParam(param *spec.Parameter, data map[string]string) {
if param == nil {
return
}
processSchema(data, param)
// schema is only allowed for parameters in "body"
// see https://swagger.io/specification/v2/#parameterObject
switch {
case param.In == "body":
param.Type = ""
case param.Schema != nil:
// convert schema into validations
param.SetValidations(param.Schema.Validations())
param.Default = param.Schema.Default
param.Format = param.Schema.Format
param.Schema = nil
}
s.parameters = append(s.parameters, param)
}
func processSchema(data map[string]string, param *spec.Parameter) {
if param.Schema == nil {
return
}
var enumValues []string
for key, value := range data {
switch key {
case SchemaMinKey:
if t := getType(param.Schema); t == TypeNumber || t == TypeInteger {
v, _ := strconv.ParseFloat(value, 64)
param.Schema.Minimum = &v
}
case SchemaMaxKey:
if t := getType(param.Schema); t == TypeNumber || t == TypeInteger {
v, _ := strconv.ParseFloat(value, 64)
param.Schema.Maximum = &v
}
case SchemaMinLenKey:
if getType(param.Schema) == TypeArray {
v, _ := strconv.ParseInt(value, 10, 64)
param.Schema.MinLength = &v
}
case SchemaMaxLenKey:
if getType(param.Schema) == TypeArray {
v, _ := strconv.ParseInt(value, 10, 64)
param.Schema.MaxLength = &v
}
case SchemaEnumKey:
enumValues = strings.Split(value, ",")
case SchemaFormatKey:
param.Schema.Format = value
case SchemaDefaultKey:
param.Schema.Default = convert(param.Type, value)
}
}
if param.Description != "" {
param.Schema.Description = param.Description
}
convertEnum(param.Schema, enumValues)
}
func convertEnum(schema *spec.Schema, enumValues []string) {
if len(enumValues) == 0 {
return
}
var finalEnum []interface{}
for _, v := range enumValues {
finalEnum = append(finalEnum, convert(schema.Type[0], strings.TrimSpace(v)))
}
schema.Enum = finalEnum
}
func convert(typeStr, valueStr string) interface{} {
switch typeStr {
case TypeInteger:
fallthrough
case TypeNumber:
if num, err := strconv.ParseFloat(valueStr, 64); err == nil {
return num
}
case TypeBoolean:
fallthrough
case TypeBool:
if b, err := strconv.ParseBool(valueStr); err == nil {
return b
}
}
return valueStr
}
func getType(schema *spec.Schema) string {
if len(schema.Type) == 0 {
return ""
}
return schema.Type[0]
}
func contains(arr []string, obj string) bool {
for _, v := range arr {
if v == obj {
return true
}
}
return false
}

View file

@ -0,0 +1,93 @@
package codescan
import (
"fmt"
"github.com/go-openapi/spec"
)
func opConsumesSetter(op *spec.Operation) func([]string) {
return func(consumes []string) { op.Consumes = consumes }
}
func opProducesSetter(op *spec.Operation) func([]string) {
return func(produces []string) { op.Produces = produces }
}
func opSchemeSetter(op *spec.Operation) func([]string) {
return func(schemes []string) { op.Schemes = schemes }
}
func opSecurityDefsSetter(op *spec.Operation) func([]map[string][]string) {
return func(securityDefs []map[string][]string) { op.Security = securityDefs }
}
func opResponsesSetter(op *spec.Operation) func(*spec.Response, map[int]spec.Response) {
return func(def *spec.Response, scr map[int]spec.Response) {
if op.Responses == nil {
op.Responses = new(spec.Responses)
}
op.Responses.Default = def
op.Responses.StatusCodeResponses = scr
}
}
func opParamSetter(op *spec.Operation) func([]*spec.Parameter) {
return func(params []*spec.Parameter) {
for _, v := range params {
op.AddParam(v)
}
}
}
func opExtensionsSetter(op *spec.Operation) func(*spec.Extensions) {
return func(exts *spec.Extensions) {
for name, value := range *exts {
op.AddExtension(name, value)
}
}
}
type routesBuilder struct {
ctx *scanCtx
route parsedPathContent
definitions map[string]spec.Schema
operations map[string]*spec.Operation
responses map[string]spec.Response
parameters []*spec.Parameter
}
func (r *routesBuilder) Build(tgt *spec.Paths) error {
pthObj := tgt.Paths[r.route.Path]
op := setPathOperation(
r.route.Method, r.route.ID,
&pthObj, r.operations[r.route.ID])
op.Tags = r.route.Tags
sp := new(sectionedParser)
sp.setTitle = func(lines []string) { op.Summary = joinDropLast(lines) }
sp.setDescription = func(lines []string) { op.Description = joinDropLast(lines) }
sr := newSetResponses(r.definitions, r.responses, opResponsesSetter(op))
spa := newSetParams(r.parameters, opParamSetter(op))
sp.taggers = []tagParser{
newMultiLineTagParser("Consumes", newMultilineDropEmptyParser(rxConsumes, opConsumesSetter(op)), false),
newMultiLineTagParser("Produces", newMultilineDropEmptyParser(rxProduces, opProducesSetter(op)), false),
newSingleLineTagParser("Schemes", newSetSchemes(opSchemeSetter(op))),
newMultiLineTagParser("Security", newSetSecurity(rxSecuritySchemes, opSecurityDefsSetter(op)), false),
newMultiLineTagParser("Parameters", spa, false),
newMultiLineTagParser("Responses", sr, false),
newSingleLineTagParser("Deprecated", &setDeprecatedOp{op}),
newMultiLineTagParser("Extensions", newSetExtensions(opExtensionsSetter(op)), true),
}
if err := sp.Parse(r.route.Remaining); err != nil {
return fmt.Errorf("operation (%s): %v", op.ID, err)
}
if tgt.Paths == nil {
tgt.Paths = make(map[string]spec.PathItem)
}
tgt.Paths[r.route.Path] = pthObj
return nil
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,258 @@
package codescan
import (
"go/ast"
"github.com/go-openapi/spec"
)
func newSpecBuilder(input *spec.Swagger, sc *scanCtx, scanModels bool) *specBuilder {
if input == nil {
input = new(spec.Swagger)
input.Swagger = "2.0"
}
if input.Paths == nil {
input.Paths = new(spec.Paths)
}
if input.Definitions == nil {
input.Definitions = make(map[string]spec.Schema)
}
if input.Responses == nil {
input.Responses = make(map[string]spec.Response)
}
if input.Extensions == nil {
input.Extensions = make(spec.Extensions)
}
return &specBuilder{
ctx: sc,
input: input,
scanModels: scanModels,
operations: collectOperationsFromInput(input),
definitions: input.Definitions,
responses: input.Responses,
}
}
type specBuilder struct {
scanModels bool
input *spec.Swagger
ctx *scanCtx
discovered []*entityDecl
definitions map[string]spec.Schema
responses map[string]spec.Response
operations map[string]*spec.Operation
}
func (s *specBuilder) Build() (*spec.Swagger, error) {
if err := s.buildModels(); err != nil {
return nil, err
}
if err := s.buildParameters(); err != nil {
return nil, err
}
if err := s.buildRespones(); err != nil {
return nil, err
}
// build definitions dictionary
if err := s.buildDiscovered(); err != nil {
return nil, err
}
if err := s.buildRoutes(); err != nil {
return nil, err
}
if err := s.buildOperations(); err != nil {
return nil, err
}
if err := s.buildMeta(); err != nil {
return nil, err
}
if s.input.Swagger == "" {
s.input.Swagger = "2.0"
}
return s.input, nil
}
func (s *specBuilder) buildDiscovered() error {
// loop over discovered until all the items are in definitions
keepGoing := len(s.discovered) > 0
for keepGoing {
var queue []*entityDecl
for _, d := range s.discovered {
nm, _ := d.Names()
if _, ok := s.definitions[nm]; !ok {
queue = append(queue, d)
}
}
s.discovered = nil
for _, sd := range queue {
if err := s.buildDiscoveredSchema(sd); err != nil {
return err
}
}
keepGoing = len(s.discovered) > 0
}
return nil
}
func (s *specBuilder) buildDiscoveredSchema(decl *entityDecl) error {
sb := &schemaBuilder{
ctx: s.ctx,
decl: decl,
discovered: s.discovered,
}
if err := sb.Build(s.definitions); err != nil {
return err
}
s.discovered = append(s.discovered, sb.postDecls...)
return nil
}
func (s *specBuilder) buildMeta() error {
// build swagger object
for _, decl := range s.ctx.app.Meta {
if err := newMetaParser(s.input).Parse(decl.Comments); err != nil {
return err
}
}
return nil
}
func (s *specBuilder) buildOperations() error {
for _, pp := range s.ctx.app.Operations {
ob := &operationsBuilder{
operations: s.operations,
ctx: s.ctx,
path: pp,
}
if err := ob.Build(s.input.Paths); err != nil {
return err
}
}
return nil
}
func (s *specBuilder) buildRoutes() error {
// build paths dictionary
for _, pp := range s.ctx.app.Routes {
rb := &routesBuilder{
ctx: s.ctx,
route: pp,
responses: s.responses,
operations: s.operations,
definitions: s.definitions,
}
if err := rb.Build(s.input.Paths); err != nil {
return err
}
}
return nil
}
func (s *specBuilder) buildRespones() error {
// build responses dictionary
for _, decl := range s.ctx.app.Responses {
rb := &responseBuilder{
ctx: s.ctx,
decl: decl,
}
if err := rb.Build(s.responses); err != nil {
return err
}
s.discovered = append(s.discovered, rb.postDecls...)
}
return nil
}
func (s *specBuilder) buildParameters() error {
// build parameters dictionary
for _, decl := range s.ctx.app.Parameters {
pb := &parameterBuilder{
ctx: s.ctx,
decl: decl,
}
if err := pb.Build(s.operations); err != nil {
return err
}
s.discovered = append(s.discovered, pb.postDecls...)
}
return nil
}
func (s *specBuilder) buildModels() error {
// build models dictionary
if !s.scanModels {
return nil
}
for _, decl := range s.ctx.app.Models {
if err := s.buildDiscoveredSchema(decl); err != nil {
return err
}
}
return s.joinExtraModels()
}
func (s *specBuilder) joinExtraModels() error {
tmp := make(map[*ast.Ident]*entityDecl, len(s.ctx.app.ExtraModels))
for k, v := range s.ctx.app.ExtraModels {
tmp[k] = v
s.ctx.app.Models[k] = v
delete(s.ctx.app.ExtraModels, k)
}
// process extra models and see if there is any reference to a new extra one
for _, decl := range tmp {
if err := s.buildDiscoveredSchema(decl); err != nil {
return err
}
}
if len(s.ctx.app.ExtraModels) > 0 {
return s.joinExtraModels()
}
return nil
}
func collectOperationsFromInput(input *spec.Swagger) map[string]*spec.Operation {
operations := make(map[string]*spec.Operation)
if input != nil && input.Paths != nil {
for _, pth := range input.Paths.Paths {
if pth.Get != nil {
operations[pth.Get.ID] = pth.Get
}
if pth.Post != nil {
operations[pth.Post.ID] = pth.Post
}
if pth.Put != nil {
operations[pth.Put.ID] = pth.Put
}
if pth.Patch != nil {
operations[pth.Patch.ID] = pth.Patch
}
if pth.Delete != nil {
operations[pth.Delete.ID] = pth.Delete
}
if pth.Head != nil {
operations[pth.Head.ID] = pth.Head
}
if pth.Options != nil {
operations[pth.Options.ID] = pth.Options
}
}
}
return operations
}

View file

@ -0,0 +1 @@
generated/

View file

@ -0,0 +1,40 @@
package generator
import (
"embed"
"io/fs"
)
//go:embed templates
var _bindata embed.FS
// AssetNames returns the names of the assets.
func AssetNames() []string {
names := make([]string, 0)
_ = fs.WalkDir(_bindata, "templates", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
names = append(names, path)
return nil
})
return names
}
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
return _bindata.ReadFile(name)
}
// MustAsset is like Asset but panics when Asset would return an error.
// It simplifies safe initialization of global variables.
func MustAsset(name string) []byte {
a, err := Asset(name)
if err != nil {
panic("asset: Asset(" + name + "): " + err.Error())
}
return a
}

View file

@ -0,0 +1,120 @@
// 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 generator
import (
"errors"
"github.com/go-openapi/swag"
)
// GenerateClient generates a client library for a swagger spec document.
func GenerateClient(name string, modelNames, operationIDs []string, opts *GenOpts) error {
if err := opts.CheckOpts(); err != nil {
return err
}
if err := opts.setTemplates(); err != nil {
return err
}
specDoc, analyzed, err := opts.analyzeSpec()
if err != nil {
return err
}
models, err := gatherModels(specDoc, modelNames)
if err != nil {
return err
}
operations := gatherOperations(analyzed, operationIDs)
if len(operations) == 0 {
return errors.New("no operations were selected")
}
generator := appGenerator{
Name: appNameOrDefault(specDoc, name, defaultClientName),
SpecDoc: specDoc,
Analyzed: analyzed,
Models: models,
Operations: operations,
Target: opts.Target,
DumpData: opts.DumpData,
Package: opts.LanguageOpts.ManglePackageName(opts.ClientPackage, defaultClientTarget),
APIPackage: opts.LanguageOpts.ManglePackagePath(opts.APIPackage, defaultOperationsTarget),
ModelsPackage: opts.LanguageOpts.ManglePackagePath(opts.ModelPackage, defaultModelsTarget),
ServerPackage: opts.LanguageOpts.ManglePackagePath(opts.ServerPackage, defaultServerTarget),
ClientPackage: opts.LanguageOpts.ManglePackagePath(opts.ClientPackage, defaultClientTarget),
OperationsPackage: opts.LanguageOpts.ManglePackagePath(opts.ClientPackage, defaultClientTarget),
Principal: opts.PrincipalAlias(),
DefaultScheme: opts.DefaultScheme,
DefaultProduces: opts.DefaultProduces,
DefaultConsumes: opts.DefaultConsumes,
GenOpts: opts,
}
generator.Receiver = "o"
return (&clientGenerator{generator}).Generate()
}
type clientGenerator struct {
appGenerator
}
func (c *clientGenerator) Generate() error {
app, err := c.makeCodegenApp()
if err != nil {
return err
}
if c.DumpData {
return dumpData(swag.ToDynamicJSON(app))
}
if c.GenOpts.IncludeModel {
for _, m := range app.Models {
if m.IsStream {
continue
}
mod := m
if err := c.GenOpts.renderDefinition(&mod); err != nil {
return err
}
}
}
if c.GenOpts.IncludeHandler {
for _, g := range app.OperationGroups {
opg := g
for _, o := range opg.Operations {
op := o
if err := c.GenOpts.renderOperation(&op); err != nil {
return err
}
}
if err := c.GenOpts.renderOperationGroup(&opg); err != nil {
return err
}
}
}
if c.GenOpts.IncludeSupport {
if err := c.GenOpts.renderApplication(&app); err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,61 @@
package generator
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/viper"
)
// LanguageDefinition in the configuration file.
type LanguageDefinition struct {
Layout SectionOpts `mapstructure:"layout"`
}
// ConfigureOpts for generation
func (d *LanguageDefinition) ConfigureOpts(opts *GenOpts) error {
opts.Sections = d.Layout
if opts.LanguageOpts == nil {
opts.LanguageOpts = GoLangOpts()
}
return nil
}
// LanguageConfig structure that is obtained from parsing a config file
type LanguageConfig map[string]LanguageDefinition
// ReadConfig at the specified path, when no path is specified it will look into
// the current directory and load a .swagger.{yml,json,hcl,toml,properties} file
// Returns a viper config or an error
func ReadConfig(fpath string) (*viper.Viper, error) {
v := viper.New()
if fpath != "" {
if !fileExists(fpath, "") {
return nil, fmt.Errorf("can't find file for %q", fpath)
}
file, err := os.Open(fpath)
if err != nil {
return nil, err
}
defer func() { _ = file.Close() }()
ext := filepath.Ext(fpath)
if len(ext) > 0 {
ext = ext[1:]
}
v.SetConfigType(ext)
if err := v.ReadConfig(file); err != nil {
return nil, err
}
return v, nil
}
v.SetConfigName(".swagger")
v.AddConfigPath(".")
if err := v.ReadInConfig(); err != nil {
if _, ok := err.(viper.UnsupportedConfigError); !ok && v.ConfigFileUsed() != "" {
return nil, err
}
}
return v, nil
}

View file

@ -0,0 +1,64 @@
// 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 generator
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
)
var (
// Debug when the env var DEBUG or SWAGGER_DEBUG is not empty
// the generators will be very noisy about what they are doing
Debug = os.Getenv("DEBUG") != "" || os.Getenv("SWAGGER_DEBUG") != ""
// generatorLogger is a debug logger for this package
generatorLogger *log.Logger
)
func debugOptions() {
generatorLogger = log.New(os.Stdout, "generator:", log.LstdFlags)
}
// debugLog wraps log.Printf with a debug-specific logger
func debugLog(frmt string, args ...interface{}) {
if Debug {
_, file, pos, _ := runtime.Caller(1)
generatorLogger.Printf("%s:%d: %s", filepath.Base(file), pos,
fmt.Sprintf(frmt, args...))
}
}
// debugLogAsJSON unmarshals its last arg as pretty JSON
func debugLogAsJSON(frmt string, args ...interface{}) {
if Debug {
var dfrmt string
_, file, pos, _ := runtime.Caller(1)
dargs := make([]interface{}, 0, len(args)+2)
dargs = append(dargs, filepath.Base(file), pos)
if len(args) > 0 {
dfrmt = "%s:%d: " + frmt + "\n%s"
bbb, _ := json.MarshalIndent(args[len(args)-1], "", " ")
dargs = append(dargs, args[0:len(args)-1]...)
dargs = append(dargs, string(bbb))
} else {
dfrmt = "%s:%d: " + frmt
}
generatorLogger.Printf(dfrmt, dargs...)
}
}

View file

@ -0,0 +1,75 @@
package generator
import (
"github.com/go-openapi/analysis"
"github.com/go-openapi/spec"
"github.com/go-openapi/swag"
)
type discInfo struct {
Discriminators map[string]discor
Discriminated map[string]discee
}
type discor struct {
FieldName string `json:"fieldName"`
GoType string `json:"goType"`
JSONName string `json:"jsonName"`
Children []discee `json:"children"`
}
type discee struct {
FieldName string `json:"fieldName"`
FieldValue string `json:"fieldValue"`
GoType string `json:"goType"`
JSONName string `json:"jsonName"`
Ref spec.Ref `json:"ref"`
ParentRef spec.Ref `json:"parentRef"`
}
func discriminatorInfo(doc *analysis.Spec) *discInfo {
baseTypes := make(map[string]discor)
for _, sch := range doc.AllDefinitions() {
if sch.Schema.Discriminator != "" {
tpe, _ := sch.Schema.Extensions.GetString(xGoName)
if tpe == "" {
tpe = swag.ToGoName(sch.Name)
}
baseTypes[sch.Ref.String()] = discor{
FieldName: sch.Schema.Discriminator,
GoType: tpe,
JSONName: sch.Name,
}
}
}
subTypes := make(map[string]discee)
for _, sch := range doc.SchemasWithAllOf() {
for _, ao := range sch.Schema.AllOf {
if ao.Ref.String() != "" {
if bt, ok := baseTypes[ao.Ref.String()]; ok {
name, _ := sch.Schema.Extensions.GetString(xClass)
if name == "" {
name = sch.Name
}
tpe, _ := sch.Schema.Extensions.GetString(xGoName)
if tpe == "" {
tpe = swag.ToGoName(sch.Name)
}
dce := discee{
FieldName: bt.FieldName,
FieldValue: name,
Ref: sch.Ref,
ParentRef: ao.Ref,
JSONName: sch.Name,
GoType: tpe,
}
subTypes[sch.Ref.String()] = dce
bt.Children = append(bt.Children, dce)
baseTypes[ao.Ref.String()] = bt
}
}
}
}
return &discInfo{Discriminators: baseTypes, Discriminated: subTypes}
}

View file

@ -0,0 +1,78 @@
// 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 generator provides the code generation library for go-swagger.
# Generating data types
The general idea is that you should rarely see interface{} in the generated code.
You get a complete representation of a swagger document in somewhat idiomatic go.
To do so, there is a set of mapping patterns that are applied,
to map a Swagger specification to go types:
definition of primitive => type alias/name
definition of array => type alias/name
definition of map => type alias/name
definition of object
with properties => struct
definition of $ref => type alias/name
object with only
additional properties => map[string]T
object with additional
properties and properties => custom serializer
schema with schema array
in items => tuple (struct with properties, custom serializer)
schema with all of => struct
* allOf schema with $ref => embedded value
* allOf schema with properties => properties are included in struct
* adding an allOf schema with just "x-isnullable": true or
"x-nullable": true turns the schema into a pointer when
there are only other extension properties provided
NOTE: anyOf and oneOf JSON-schema constructs are not supported by Swagger 2.0
A property on a definition is a pointer when any one of the following conditions is met:
it is an object schema (struct)
it has x-nullable or x-isnullable as vendor extension
it is a primitive where the zero value is valid but would fail validation
otherwise strings minLength > 0 or required results in non-pointer
numbers min > 0, max < 0 and min < max
JSONSchema and by extension Swagger allow for items that have a fixed size array,
with the schema describing the items at each index. This can be combined with additional items
to form some kind of tuple with varargs.
To map this to go it creates a struct that has fixed names and a custom json serializer.
NOTE: the additionalItems keyword is not supported by Swagger 2.0. However, the generator and validator parts
in go-swagger do.
# Documenting the generated code
The code that is generated also gets the doc comments that are used by the scanner
to generate a spec from go code. So that after generation you should be able to reverse
generate a spec from the code that was generated by your spec.
It should be equivalent to the original spec but might miss some default values and examples.
*/
package generator

View file

@ -0,0 +1,226 @@
// 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 generator
// TODO: we may probably find a way to register most of this dynamically from strfmt
// map of function calls to be generated to get the zero value of a given type
var zeroes = map[string]string{
"bool": "false",
"float32": "0",
"float64": "0",
"int": "0",
"int8": "0",
"int16": "0",
"int32": "0",
"int64": "0",
"string": "\"\"",
"uint": "0",
"uint8": "0",
"uint16": "0",
"uint32": "0",
"uint64": "0",
// Extended formats (23 formats corresponding to the Default registry
// provided by go-openapi/strfmt)
"strfmt.Base64": "strfmt.Base64([]byte(nil))",
"strfmt.CreditCard": "strfmt.CreditCard(\"\")",
"strfmt.Date": "strfmt.Date{}",
"strfmt.DateTime": "strfmt.DateTime{}",
"strfmt.Duration": "strfmt.Duration(0)",
"strfmt.Email": "strfmt.Email(\"\")",
"strfmt.HexColor": "strfmt.HexColor(\"#000000\")",
"strfmt.Hostname": "strfmt.Hostname(\"\")",
"strfmt.IPv4": "strfmt.IPv4(\"\")",
"strfmt.IPv6": "strfmt.IPv6(\"\")",
"strfmt.ISBN": "strfmt.ISBN(\"\")",
"strfmt.ISBN10": "strfmt.ISBN10(\"\")",
"strfmt.ISBN13": "strfmt.ISBN13(\"\")",
"strfmt.MAC": "strfmt.MAC(\"\")",
"strfmt.ObjectId": "strfmt.ObjectId{}",
"strfmt.Password": "strfmt.Password(\"\")",
"strfmt.RGBColor": "strfmt.RGBColor(\"rgb(0,0,0)\")",
"strfmt.SSN": "strfmt.SSN(\"\")",
"strfmt.URI": "strfmt.URI(\"\")",
"strfmt.UUID": "strfmt.UUID(\"\")",
"strfmt.UUID3": "strfmt.UUID3(\"\")",
"strfmt.UUID4": "strfmt.UUID4(\"\")",
"strfmt.UUID5": "strfmt.UUID5(\"\")",
// "file": "runtime.File",
}
// conversion functions from string representation to a numerical or boolean
// primitive type
var stringConverters = map[string]string{
"bool": "swag.ConvertBool",
"float32": "swag.ConvertFloat32",
"float64": "swag.ConvertFloat64",
"int8": "swag.ConvertInt8",
"int16": "swag.ConvertInt16",
"int32": "swag.ConvertInt32",
"int64": "swag.ConvertInt64",
"uint8": "swag.ConvertUint8",
"uint16": "swag.ConvertUint16",
"uint32": "swag.ConvertUint32",
"uint64": "swag.ConvertUint64",
}
// formatting (string representation) functions from a native representation
// of a numerical or boolean primitive type
var stringFormatters = map[string]string{
"bool": "swag.FormatBool",
"float32": "swag.FormatFloat32",
"float64": "swag.FormatFloat64",
"int8": "swag.FormatInt8",
"int16": "swag.FormatInt16",
"int32": "swag.FormatInt32",
"int64": "swag.FormatInt64",
"uint8": "swag.FormatUint8",
"uint16": "swag.FormatUint16",
"uint32": "swag.FormatUint32",
"uint64": "swag.FormatUint64",
}
// typeMapping contains a mapping of type name to go type
var typeMapping = map[string]string{
// Standard formats with native, straightforward, mapping
"string": "string",
"boolean": "bool",
"integer": "int64",
"number": "float64",
// For file producers
"file": "runtime.File",
}
// formatMapping contains a type-specific version of mapping of format to go type
var formatMapping = map[string]map[string]string{
"number": {
"double": "float64",
"float": "float32",
"int": "int64",
"int8": "int8",
"int16": "int16",
"int32": "int32",
"int64": "int64",
"uint": "uint64",
"uint8": "uint8",
"uint16": "uint16",
"uint32": "uint32",
"uint64": "uint64",
},
"integer": {
"int": "int64",
"int8": "int8",
"int16": "int16",
"int32": "int32",
"int64": "int64",
"uint": "uint64",
"uint8": "uint8",
"uint16": "uint16",
"uint32": "uint32",
"uint64": "uint64",
},
"string": {
"char": "rune",
// Extended format registry from go-openapi/strfmt.
// Currently, 23 such formats are supported (default strftm registry),
// plus the following aliases:
// - "datetime" alias for the more official "date-time"
// - "objectid" and "ObjectId" aliases for "bsonobjectid"
"binary": "io.ReadCloser",
"byte": "strfmt.Base64",
"creditcard": "strfmt.CreditCard",
"date": "strfmt.Date",
"date-time": "strfmt.DateTime",
"datetime": "strfmt.DateTime",
"duration": "strfmt.Duration",
"email": "strfmt.Email",
"hexcolor": "strfmt.HexColor",
"hostname": "strfmt.Hostname",
"ipv4": "strfmt.IPv4",
"ipv6": "strfmt.IPv6",
"isbn": "strfmt.ISBN",
"isbn10": "strfmt.ISBN10",
"isbn13": "strfmt.ISBN13",
"mac": "strfmt.MAC",
"bsonobjectid": "strfmt.ObjectId",
"objectid": "strfmt.ObjectId",
"ObjectId": "strfmt.ObjectId", // NOTE: does it work with uppercase?
"password": "strfmt.Password",
"rgbcolor": "strfmt.RGBColor",
"ssn": "strfmt.SSN",
"uri": "strfmt.URI",
"uuid": "strfmt.UUID",
"uuid3": "strfmt.UUID3",
"uuid4": "strfmt.UUID4",
"uuid5": "strfmt.UUID5",
// For file producers
"file": "runtime.File",
},
}
// go primitive types
var primitives = map[string]struct{}{
"bool": {},
"byte": {},
"[]byte": {},
"complex64": {},
"complex128": {},
"float32": {},
"float64": {},
"int": {},
"int8": {},
"int16": {},
"int32": {},
"int64": {},
"rune": {},
"string": {},
"uint": {},
"uint8": {},
"uint16": {},
"uint32": {},
"uint64": {},
}
// Formats with a custom formatter.
// Currently, 23 such formats are supported
var customFormatters = map[string]struct{}{
"strfmt.Base64": {},
"strfmt.CreditCard": {},
"strfmt.Date": {},
"strfmt.DateTime": {},
"strfmt.Duration": {},
"strfmt.Email": {},
"strfmt.HexColor": {},
"strfmt.Hostname": {},
"strfmt.IPv4": {},
"strfmt.IPv6": {},
"strfmt.ISBN": {},
"strfmt.ISBN10": {},
"strfmt.ISBN13": {},
"strfmt.MAC": {},
"strfmt.ObjectId": {},
"strfmt.Password": {},
"strfmt.RGBColor": {},
"strfmt.SSN": {},
"strfmt.URI": {},
"strfmt.UUID": {},
"strfmt.UUID3": {},
"strfmt.UUID4": {},
"strfmt.UUID5": {},
// the following interfaces do not generate validations
"io.ReadCloser": {}, // for "format": "binary" (server side)
"io.Writer": {}, // for "format": "binary" (client side)
// NOTE: runtime.File is not a customFormatter
}

View file

@ -0,0 +1,50 @@
//go:build !windows
// +build !windows
package generator
import (
"log"
"plugin"
"text/template"
)
type GenOpts struct {
GenOptsCommon
TemplatePlugin string
}
func (g *GenOpts) setTemplates() error {
if g.TemplatePlugin != "" {
if err := g.templates.LoadPlugin(g.TemplatePlugin); err != nil {
return err
}
}
return g.GenOptsCommon.setTemplates()
}
// LoadPlugin will load the named plugin and inject its functions into the funcMap
//
// The plugin must implement a function matching the signature:
// `func AddFuncs(f template.FuncMap)`
// which can add any number of functions to the template repository funcMap.
// Any existing sprig or go-swagger templates with the same name will be overridden.
func (t *Repository) LoadPlugin(pluginPath string) error {
log.Printf("Attempting to load template plugin: %s", pluginPath)
p, err := plugin.Open(pluginPath)
if err != nil {
return err
}
f, err := p.Lookup("AddFuncs")
if err != nil {
return err
}
f.(func(template.FuncMap))(t.funcs)
return nil
}

View file

@ -0,0 +1,12 @@
//go:build windows
// +build windows
package generator
type GenOpts struct {
GenOptsCommon
}
func (g *GenOpts) setTemplates() error {
return g.GenOptsCommon.setTemplates()
}

View file

@ -0,0 +1,440 @@
package generator
import (
"encoding/json"
"fmt"
"io"
"log"
"os"
"path"
"path/filepath"
"regexp"
goruntime "runtime"
"sort"
"strings"
"github.com/go-openapi/swag"
"golang.org/x/tools/imports"
)
var (
// DefaultLanguageFunc defines the default generation language
DefaultLanguageFunc func() *LanguageOpts
moduleRe *regexp.Regexp
)
func initLanguage() {
DefaultLanguageFunc = GoLangOpts
moduleRe = regexp.MustCompile(`module[ \t]+([^\s]+)`)
}
// LanguageOpts to describe a language to the code generator
type LanguageOpts struct {
ReservedWords []string
BaseImportFunc func(string) string `json:"-"`
ImportsFunc func(map[string]string) string `json:"-"`
ArrayInitializerFunc func(interface{}) (string, error) `json:"-"`
reservedWordsSet map[string]struct{}
initialized bool
formatFunc func(string, []byte) ([]byte, error)
fileNameFunc func(string) string // language specific source file naming rules
dirNameFunc func(string) string // language specific directory naming rules
}
// Init the language option
func (l *LanguageOpts) Init() {
if l.initialized {
return
}
l.initialized = true
l.reservedWordsSet = make(map[string]struct{})
for _, rw := range l.ReservedWords {
l.reservedWordsSet[rw] = struct{}{}
}
}
// MangleName makes sure a reserved word gets a safe name
func (l *LanguageOpts) MangleName(name, suffix string) string {
if _, ok := l.reservedWordsSet[swag.ToFileName(name)]; !ok {
return name
}
return strings.Join([]string{name, suffix}, "_")
}
// MangleVarName makes sure a reserved word gets a safe name
func (l *LanguageOpts) MangleVarName(name string) string {
nm := swag.ToVarName(name)
if _, ok := l.reservedWordsSet[nm]; !ok {
return nm
}
return nm + "Var"
}
// MangleFileName makes sure a file name gets a safe name
func (l *LanguageOpts) MangleFileName(name string) string {
if l.fileNameFunc != nil {
return l.fileNameFunc(name)
}
return swag.ToFileName(name)
}
// ManglePackageName makes sure a package gets a safe name.
// In case of a file system path (e.g. name contains "/" or "\" on Windows), this return only the last element.
func (l *LanguageOpts) ManglePackageName(name, suffix string) string {
if name == "" {
return suffix
}
if l.dirNameFunc != nil {
name = l.dirNameFunc(name)
}
pth := filepath.ToSlash(filepath.Clean(name)) // preserve path
pkg := importAlias(pth) // drop path
return l.MangleName(swag.ToFileName(prefixForName(pkg)+pkg), suffix)
}
// ManglePackagePath makes sure a full package path gets a safe name.
// Only the last part of the path is altered.
func (l *LanguageOpts) ManglePackagePath(name string, suffix string) string {
if name == "" {
return suffix
}
target := filepath.ToSlash(filepath.Clean(name)) // preserve path
parts := strings.Split(target, "/")
parts[len(parts)-1] = l.ManglePackageName(parts[len(parts)-1], suffix)
return strings.Join(parts, "/")
}
// FormatContent formats a file with a language specific formatter
func (l *LanguageOpts) FormatContent(name string, content []byte) ([]byte, error) {
if l.formatFunc != nil {
return l.formatFunc(name, content)
}
return content, nil
}
// imports generate the code to import some external packages, possibly aliased
func (l *LanguageOpts) imports(imports map[string]string) string {
if l.ImportsFunc != nil {
return l.ImportsFunc(imports)
}
return ""
}
// arrayInitializer builds a litteral array
func (l *LanguageOpts) arrayInitializer(data interface{}) (string, error) {
if l.ArrayInitializerFunc != nil {
return l.ArrayInitializerFunc(data)
}
return "", nil
}
// baseImport figures out the base path to generate import statements
func (l *LanguageOpts) baseImport(tgt string) string {
if l.BaseImportFunc != nil {
return l.BaseImportFunc(tgt)
}
debugLog("base import func is nil")
return ""
}
// GoLangOpts for rendering items as golang code
func GoLangOpts() *LanguageOpts {
var goOtherReservedSuffixes = map[string]bool{
// see:
// https://golang.org/src/go/build/syslist.go
// https://golang.org/doc/install/source#environment
// goos
"aix": true,
"android": true,
"darwin": true,
"dragonfly": true,
"freebsd": true,
"hurd": true,
"illumos": true,
"js": true,
"linux": true,
"nacl": true,
"netbsd": true,
"openbsd": true,
"plan9": true,
"solaris": true,
"windows": true,
"zos": true,
// arch
"386": true,
"amd64": true,
"amd64p32": true,
"arm": true,
"armbe": true,
"arm64": true,
"arm64be": true,
"mips": true,
"mipsle": true,
"mips64": true,
"mips64le": true,
"mips64p32": true,
"mips64p32le": true,
"ppc": true,
"ppc64": true,
"ppc64le": true,
"riscv": true,
"riscv64": true,
"s390": true,
"s390x": true,
"sparc": true,
"sparc64": true,
"wasm": true,
// other reserved suffixes
"test": true,
}
opts := new(LanguageOpts)
opts.ReservedWords = []string{
"break", "default", "func", "interface", "select",
"case", "defer", "go", "map", "struct",
"chan", "else", "goto", "package", "switch",
"const", "fallthrough", "if", "range", "type",
"continue", "for", "import", "return", "var",
}
opts.formatFunc = func(ffn string, content []byte) ([]byte, error) {
opts := new(imports.Options)
opts.TabIndent = true
opts.TabWidth = 2
opts.Fragment = true
opts.Comments = true
return imports.Process(ffn, content, opts)
}
opts.fileNameFunc = func(name string) string {
// whenever a generated file name ends with a suffix
// that is meaningful to go build, adds a "swagger"
// suffix
parts := strings.Split(swag.ToFileName(name), "_")
if goOtherReservedSuffixes[parts[len(parts)-1]] {
// file name ending with a reserved arch or os name
// are appended an innocuous suffix "swagger"
parts = append(parts, "swagger")
}
return strings.Join(parts, "_")
}
opts.dirNameFunc = func(name string) string {
// whenever a generated directory name is a special
// golang directory, append an innocuous suffix
switch name {
case "vendor", "internal":
return strings.Join([]string{name, "swagger"}, "_")
}
return name
}
opts.ImportsFunc = func(imports map[string]string) string {
if len(imports) == 0 {
return ""
}
result := make([]string, 0, len(imports))
for k, v := range imports {
_, name := path.Split(v)
if name != k {
result = append(result, fmt.Sprintf("\t%s %q", k, v))
} else {
result = append(result, fmt.Sprintf("\t%q", v))
}
}
sort.Strings(result)
return strings.Join(result, "\n")
}
opts.ArrayInitializerFunc = func(data interface{}) (string, error) {
// ArrayInitializer constructs a Go literal initializer from interface{} literals.
// e.g. []interface{}{"a", "b"} is transformed in {"a","b",}
// e.g. map[string]interface{}{ "a": "x", "b": "y"} is transformed in {"a":"x","b":"y",}.
//
// NOTE: this is currently used to construct simple slice intializers for default values.
// This allows for nicer slice initializers for slices of primitive types and avoid systematic use for json.Unmarshal().
b, err := json.Marshal(data)
if err != nil {
return "", err
}
return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(string(b), "}", ",}"), "[", "{"), "]", ",}"), "{,}", "{}"), nil
}
opts.BaseImportFunc = func(tgt string) string {
tgt = filepath.Clean(tgt)
// On Windows, filepath.Abs("") behaves differently than on Unix.
// Windows: yields an error, since Abs() does not know the volume.
// UNIX: returns current working directory
if tgt == "" {
tgt = "."
}
tgtAbsPath, err := filepath.Abs(tgt)
if err != nil {
log.Fatalf("could not evaluate base import path with target \"%s\": %v", tgt, err)
}
var tgtAbsPathExtended string
tgtAbsPathExtended, err = filepath.EvalSymlinks(tgtAbsPath)
if err != nil {
log.Fatalf("could not evaluate base import path with target \"%s\" (with symlink resolution): %v", tgtAbsPath, err)
}
gopath := os.Getenv("GOPATH")
if gopath == "" {
homeDir, herr := os.UserHomeDir()
if herr != nil {
log.Fatalln(herr)
}
gopath = filepath.Join(homeDir, "go")
}
var pth string
for _, gp := range filepath.SplitList(gopath) {
if _, derr := os.Stat(filepath.Join(gp, "src")); os.IsNotExist(derr) {
continue
}
// EvalSymLinks also calls the Clean
gopathExtended, er := filepath.EvalSymlinks(gp)
if er != nil {
panic(er)
}
gopathExtended = filepath.Join(gopathExtended, "src")
gp = filepath.Join(gp, "src")
// At this stage we have expanded and unexpanded target path. GOPATH is fully expanded.
// Expanded means symlink free.
// We compare both types of targetpath<s> with gopath.
// If any one of them coincides with gopath , it is imperative that
// target path lies inside gopath. How?
// - Case 1: Irrespective of symlinks paths coincide. Both non-expanded paths.
// - Case 2: Symlink in target path points to location inside GOPATH. (Expanded Target Path)
// - Case 3: Symlink in target path points to directory outside GOPATH (Unexpanded target path)
// Case 1: - Do nothing case. If non-expanded paths match just generate base import path as if
// there are no symlinks.
// Case 2: - Symlink in target path points to location inside GOPATH. (Expanded Target Path)
// First if will fail. Second if will succeed.
// Case 3: - Symlink in target path points to directory outside GOPATH (Unexpanded target path)
// First if will succeed and break.
// compares non expanded path for both
if ok, relativepath := checkPrefixAndFetchRelativePath(tgtAbsPath, gp); ok {
pth = relativepath
break
}
// Compares non-expanded target path
if ok, relativepath := checkPrefixAndFetchRelativePath(tgtAbsPath, gopathExtended); ok {
pth = relativepath
break
}
// Compares expanded target path.
if ok, relativepath := checkPrefixAndFetchRelativePath(tgtAbsPathExtended, gopathExtended); ok {
pth = relativepath
break
}
}
mod, goModuleAbsPath, err := tryResolveModule(tgtAbsPath)
switch {
case err != nil:
log.Fatalf("Failed to resolve module using go.mod file: %s", err)
case mod != "":
relTgt := relPathToRelGoPath(goModuleAbsPath, tgtAbsPath)
if !strings.HasSuffix(mod, relTgt) {
return filepath.ToSlash(mod + relTgt)
}
return filepath.ToSlash(mod)
}
if pth == "" {
log.Fatalln("target must reside inside a location in the $GOPATH/src or be a module")
}
return filepath.ToSlash(pth)
}
opts.Init()
return opts
}
// resolveGoModFile walks up the directory tree starting from 'dir' until it
// finds a go.mod file. If go.mod is found it will return the related file
// object. If no go.mod file is found it will return an error.
func resolveGoModFile(dir string) (*os.File, string, error) {
goModPath := filepath.Join(dir, "go.mod")
f, err := os.Open(goModPath)
if err != nil {
if os.IsNotExist(err) && dir != filepath.Dir(dir) {
return resolveGoModFile(filepath.Dir(dir))
}
return nil, "", err
}
return f, dir, nil
}
// relPathToRelGoPath takes a relative os path and returns the relative go
// package path. For unix nothing will change but for windows \ will be
// converted to /.
func relPathToRelGoPath(modAbsPath, absPath string) string {
if absPath == "." {
return ""
}
path := strings.TrimPrefix(absPath, modAbsPath)
pathItems := strings.Split(path, string(filepath.Separator))
return strings.Join(pathItems, "/")
}
func tryResolveModule(baseTargetPath string) (string, string, error) {
f, goModAbsPath, err := resolveGoModFile(baseTargetPath)
switch {
case os.IsNotExist(err):
return "", "", nil
case err != nil:
return "", "", err
}
src, err := io.ReadAll(f)
if err != nil {
return "", "", err
}
match := moduleRe.FindSubmatch(src)
if len(match) != 2 {
return "", "", nil
}
return string(match[1]), goModAbsPath, nil
}
// 1. Checks if the child path and parent path coincide.
// 2. If they do return child path relative to parent path.
// 3. Everything else return false
func checkPrefixAndFetchRelativePath(childpath string, parentpath string) (bool, string) {
// Windows (local) file systems - NTFS, as well as FAT and variants
// are case insensitive.
cp, pp := childpath, parentpath
if goruntime.GOOS == "windows" {
cp = strings.ToLower(cp)
pp = strings.ToLower(pp)
}
if strings.HasPrefix(cp, pp) {
pth, err := filepath.Rel(parentpath, childpath)
if err != nil {
log.Fatalln(err)
}
return true, pth
}
return false, ""
}

View file

@ -0,0 +1,191 @@
package generator
import (
"regexp"
"sort"
"strings"
"github.com/go-openapi/runtime"
"github.com/go-openapi/swag"
)
const jsonSerializer = "json"
var mediaTypeNames = map[*regexp.Regexp]string{
regexp.MustCompile("application/.*json"): jsonSerializer,
regexp.MustCompile("application/.*yaml"): "yaml",
regexp.MustCompile("application/.*protobuf"): "protobuf",
regexp.MustCompile("application/.*capnproto"): "capnproto",
regexp.MustCompile("application/.*thrift"): "thrift",
regexp.MustCompile("(?:application|text)/.*xml"): "xml",
regexp.MustCompile("text/.*markdown"): "markdown",
regexp.MustCompile("text/.*html"): "html",
regexp.MustCompile("text/.*csv"): "csv",
regexp.MustCompile("text/.*tsv"): "tsv",
regexp.MustCompile("text/.*javascript"): "js",
regexp.MustCompile("text/.*css"): "css",
regexp.MustCompile("text/.*plain"): "txt",
regexp.MustCompile("application/.*octet-stream"): "bin",
regexp.MustCompile("application/.*tar"): "tar",
regexp.MustCompile("application/.*gzip"): "gzip",
regexp.MustCompile("application/.*gz"): "gzip",
regexp.MustCompile("application/.*raw-stream"): "bin",
regexp.MustCompile("application/x-www-form-urlencoded"): "urlform",
regexp.MustCompile("application/javascript"): "txt",
regexp.MustCompile("multipart/form-data"): "multipartform",
regexp.MustCompile("image/.*"): "bin",
regexp.MustCompile("audio/.*"): "bin",
regexp.MustCompile("application/pdf"): "bin",
}
var knownProducers = map[string]string{
jsonSerializer: "runtime.JSONProducer()",
"yaml": "yamlpc.YAMLProducer()",
"xml": "runtime.XMLProducer()",
"txt": "runtime.TextProducer()",
"bin": "runtime.ByteStreamProducer()",
"urlform": "runtime.DiscardProducer",
"multipartform": "runtime.DiscardProducer",
}
var knownConsumers = map[string]string{
jsonSerializer: "runtime.JSONConsumer()",
"yaml": "yamlpc.YAMLConsumer()",
"xml": "runtime.XMLConsumer()",
"txt": "runtime.TextConsumer()",
"bin": "runtime.ByteStreamConsumer()",
"urlform": "runtime.DiscardConsumer",
"multipartform": "runtime.DiscardConsumer",
}
func wellKnownMime(tn string) (string, bool) {
for k, v := range mediaTypeNames {
if k.MatchString(tn) {
return v, true
}
}
return "", false
}
func mediaMime(orig string) string {
return strings.SplitN(orig, ";", 2)[0]
}
func mediaParameters(orig string) string {
parts := strings.SplitN(orig, ";", 2)
if len(parts) < 2 {
return ""
}
return parts[1]
}
func (a *appGenerator) makeSerializers(mediaTypes []string, known func(string) (string, bool)) (GenSerGroups, bool) {
supportsJSON := false
uniqueSerializers := make(map[string]*GenSerializer, len(mediaTypes))
uniqueSerializerGroups := make(map[string]*GenSerGroup, len(mediaTypes))
// build all required serializers
for _, media := range mediaTypes {
key := mediaMime(media)
nm, ok := wellKnownMime(key)
if !ok {
// keep this serializer named, even though its implementation is empty (cf. #1557)
nm = key
}
name := swag.ToJSONName(nm)
impl, _ := known(name)
ser, ok := uniqueSerializers[key]
if !ok {
ser = &GenSerializer{
AppName: a.Name,
ReceiverName: a.Receiver,
Name: name,
MediaType: key,
Implementation: impl,
Parameters: []string{},
}
uniqueSerializers[key] = ser
}
// provide all known parameters (currently unused by codegen templates)
if params := strings.TrimSpace(mediaParameters(media)); params != "" {
found := false
for _, p := range ser.Parameters {
if params == p {
found = true
break
}
}
if !found {
ser.Parameters = append(ser.Parameters, params)
}
}
uniqueSerializerGroups[name] = &GenSerGroup{
GenSerializer: GenSerializer{
AppName: a.Name,
ReceiverName: a.Receiver,
Name: name,
Implementation: impl,
},
}
}
if len(uniqueSerializers) == 0 {
impl, _ := known(jsonSerializer)
uniqueSerializers[runtime.JSONMime] = &GenSerializer{
AppName: a.Name,
ReceiverName: a.Receiver,
Name: jsonSerializer,
MediaType: runtime.JSONMime,
Implementation: impl,
Parameters: []string{},
}
uniqueSerializerGroups[jsonSerializer] = &GenSerGroup{
GenSerializer: GenSerializer{
AppName: a.Name,
ReceiverName: a.Receiver,
Name: jsonSerializer,
Implementation: impl,
},
}
supportsJSON = true
}
// group serializers by consumer/producer to serve several mime media types
serializerGroups := make(GenSerGroups, 0, len(uniqueSerializers))
for _, group := range uniqueSerializerGroups {
if group.Name == jsonSerializer {
supportsJSON = true
}
serializers := make(GenSerializers, 0, len(uniqueSerializers))
for _, ser := range uniqueSerializers {
if group.Name == ser.Name {
sort.Strings(ser.Parameters)
serializers = append(serializers, *ser)
}
}
sort.Sort(serializers)
group.AllSerializers = serializers // provides the full list of mime media types for this serializer group
serializerGroups = append(serializerGroups, *group)
}
sort.Sort(serializerGroups)
return serializerGroups, supportsJSON
}
func (a *appGenerator) makeConsumes() (GenSerGroups, bool) {
// builds a codegen struct from all consumes in the spec
return a.makeSerializers(a.Analyzed.RequiredConsumes(), func(media string) (string, bool) {
c, ok := knownConsumers[media]
return c, ok
})
}
func (a *appGenerator) makeProduces() (GenSerGroups, bool) {
// builds a codegen struct from all produces in the spec
return a.makeSerializers(a.Analyzed.RequiredProduces(), func(media string) (string, bool) {
p, ok := knownProducers[media]
return p, ok
})
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,273 @@
package generator
import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"github.com/go-openapi/analysis"
swaggererrors "github.com/go-openapi/errors"
"github.com/go-openapi/loads"
"github.com/go-openapi/spec"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
yamlv2 "gopkg.in/yaml.v2"
)
func (g *GenOpts) validateAndFlattenSpec() (*loads.Document, error) {
// Load spec document
specDoc, err := loads.Spec(g.Spec)
if err != nil {
return nil, err
}
// If accepts definitions only, add dummy swagger header to pass validation
if g.AcceptDefinitionsOnly {
specDoc, err = applyDefaultSwagger(specDoc)
if err != nil {
return nil, err
}
}
// Validate if needed
if g.ValidateSpec {
log.Printf("validating spec %v", g.Spec)
validationErrors := validate.Spec(specDoc, strfmt.Default)
if validationErrors != nil {
str := fmt.Sprintf("The swagger spec at %q is invalid against swagger specification %s. see errors :\n",
g.Spec, specDoc.Version())
for _, desc := range validationErrors.(*swaggererrors.CompositeError).Errors {
str += fmt.Sprintf("- %s\n", desc)
}
return nil, errors.New(str)
}
// TODO(fredbi): due to uncontrolled $ref state in spec, we need to reload the spec atm, or flatten won't
// work properly (validate expansion alters the $ref cache in go-openapi/spec)
specDoc, _ = loads.Spec(g.Spec)
}
// Flatten spec
//
// Some preprocessing is required before codegen
//
// This ensures at least that $ref's in the spec document are canonical,
// i.e all $ref are local to this file and point to some uniquely named definition.
//
// Default option is to ensure minimal flattening of $ref, bundling remote $refs and relocating arbitrary JSON
// pointers as definitions.
// This preprocessing may introduce duplicate names (e.g. remote $ref with same name). In this case, a definition
// suffixed with "OAIGen" is produced.
//
// Full flattening option farther transforms the spec by moving every complex object (e.g. with some properties)
// as a standalone definition.
//
// Eventually, an "expand spec" option is available. It is essentially useful for testing purposes.
//
// NOTE(fredbi): spec expansion may produce some unsupported constructs and is not yet protected against the
// following cases:
// - polymorphic types generation may fail with expansion (expand destructs the reuse intent of the $ref in allOf)
// - name duplicates may occur and result in compilation failures
//
// The right place to fix these shortcomings is go-openapi/analysis.
g.FlattenOpts.BasePath = specDoc.SpecFilePath()
g.FlattenOpts.Spec = analysis.New(specDoc.Spec())
g.printFlattenOpts()
if err = analysis.Flatten(*g.FlattenOpts); err != nil {
return nil, err
}
// yields the preprocessed spec document
return specDoc, nil
}
func (g *GenOpts) analyzeSpec() (*loads.Document, *analysis.Spec, error) {
// load, validate and flatten
specDoc, err := g.validateAndFlattenSpec()
if err != nil {
return nil, nil, err
}
// spec preprocessing option
if g.PropertiesSpecOrder {
g.Spec = WithAutoXOrder(g.Spec)
specDoc, err = loads.Spec(g.Spec)
if err != nil {
return nil, nil, err
}
}
// analyze the spec
analyzed := analysis.New(specDoc.Spec())
return specDoc, analyzed, nil
}
func (g *GenOpts) printFlattenOpts() {
var preprocessingOption string
switch {
case g.FlattenOpts.Expand:
preprocessingOption = "expand"
case g.FlattenOpts.Minimal:
preprocessingOption = "minimal flattening"
default:
preprocessingOption = "full flattening"
}
log.Printf("preprocessing spec with option: %s", preprocessingOption)
}
// findSwaggerSpec fetches a default swagger spec if none is provided
func findSwaggerSpec(nm string) (string, error) {
specs := []string{"swagger.json", "swagger.yml", "swagger.yaml"}
if nm != "" {
specs = []string{nm}
}
var name string
for _, nn := range specs {
f, err := os.Stat(nn)
if err != nil {
if os.IsNotExist(err) {
continue
}
return "", err
}
if f.IsDir() {
return "", fmt.Errorf("%s is a directory", nn)
}
name = nn
break
}
if name == "" {
return "", errors.New("couldn't find a swagger spec")
}
return name, nil
}
// WithAutoXOrder amends the spec to specify property order as they appear
// in the spec (supports yaml documents only).
func WithAutoXOrder(specPath string) string {
lookFor := func(ele interface{}, key string) (yamlv2.MapSlice, bool) {
if slice, ok := ele.(yamlv2.MapSlice); ok {
for _, v := range slice {
if v.Key == key {
if slice, ok := v.Value.(yamlv2.MapSlice); ok {
return slice, ok
}
}
}
}
return nil, false
}
var addXOrder func(interface{})
addXOrder = func(element interface{}) {
if props, ok := lookFor(element, "properties"); ok {
for i, prop := range props {
if pSlice, ok := prop.Value.(yamlv2.MapSlice); ok {
isObject := false
xOrderIndex := -1 // find if x-order already exists
for i, v := range pSlice {
if v.Key == "type" && v.Value == object {
isObject = true
}
if v.Key == xOrder {
xOrderIndex = i
break
}
}
if xOrderIndex > -1 { // override existing x-order
pSlice[xOrderIndex] = yamlv2.MapItem{Key: xOrder, Value: i}
} else { // append new x-order
pSlice = append(pSlice, yamlv2.MapItem{Key: xOrder, Value: i})
}
prop.Value = pSlice
props[i] = prop
if isObject {
addXOrder(pSlice)
}
}
}
}
}
data, err := swag.LoadFromFileOrHTTP(specPath)
if err != nil {
panic(err)
}
yamlDoc, err := BytesToYAMLv2Doc(data)
if err != nil {
panic(err)
}
if defs, ok := lookFor(yamlDoc, "definitions"); ok {
for _, def := range defs {
addXOrder(def.Value)
}
}
addXOrder(yamlDoc)
out, err := yamlv2.Marshal(yamlDoc)
if err != nil {
panic(err)
}
tmpDir, err := os.MkdirTemp("", "go-swagger-")
if err != nil {
panic(err)
}
tmpFile := filepath.Join(tmpDir, filepath.Base(specPath))
if err := os.WriteFile(tmpFile, out, 0600); err != nil {
panic(err)
}
return tmpFile
}
// BytesToYAMLDoc converts a byte slice into a YAML document
func BytesToYAMLv2Doc(data []byte) (interface{}, error) {
var canary map[interface{}]interface{} // validate this is an object and not a different type
if err := yamlv2.Unmarshal(data, &canary); err != nil {
return nil, err
}
var document yamlv2.MapSlice // preserve order that is present in the document
if err := yamlv2.Unmarshal(data, &document); err != nil {
return nil, err
}
return document, nil
}
func applyDefaultSwagger(doc *loads.Document) (*loads.Document, error) {
// bake a minimal swagger spec to pass validation
swspec := doc.Spec()
if swspec.Swagger == "" {
swspec.Swagger = "2.0"
}
if swspec.Info == nil {
info := new(spec.Info)
info.Version = "0.0.0"
info.Title = "minimal"
swspec.Info = info
}
if swspec.Paths == nil {
swspec.Paths = &spec.Paths{}
}
// rewrite the document with the new addition
jazon, err := json.Marshal(swspec)
if err != nil {
return nil, err
}
return loads.Analyzed(jazon, swspec.Swagger)
}

View file

@ -0,0 +1,803 @@
package generator
import (
"bytes"
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
"github.com/go-openapi/analysis"
"github.com/go-openapi/spec"
)
// GenCommon contains common properties needed across
// definitions, app and operations
// TargetImportPath may be used by templates to import other (possibly
// generated) packages in the generation path (e.g. relative to GOPATH).
// TargetImportPath is NOT used by standard templates.
type GenCommon struct {
Copyright string
TargetImportPath string
}
// GenDefinition contains all the properties to generate a
// definition from a swagger spec
type GenDefinition struct {
GenCommon
GenSchema
Package string
Imports map[string]string
DefaultImports map[string]string
ExtraSchemas GenSchemaList
DependsOn []string
External bool
}
// GenDefinitions represents a list of operations to generate
// this implements a sort by operation id
type GenDefinitions []GenDefinition
func (g GenDefinitions) Len() int { return len(g) }
func (g GenDefinitions) Less(i, j int) bool { return g[i].Name < g[j].Name }
func (g GenDefinitions) Swap(i, j int) { g[i], g[j] = g[j], g[i] }
// GenSchemaList is a list of schemas for generation.
//
// It can be sorted by name to get a stable struct layout for
// version control and such
type GenSchemaList []GenSchema
// GenSchema contains all the information needed to generate the code
// for a schema
type GenSchema struct {
resolvedType
sharedValidations
Example string
OriginalName string
Name string
Suffix string
Path string
ValueExpression string
IndexVar string
KeyVar string
Title string
Description string
Location string
ReceiverName string
Items *GenSchema
AllowsAdditionalItems bool
HasAdditionalItems bool
AdditionalItems *GenSchema
Object *GenSchema
XMLName string
CustomTag string
Properties GenSchemaList
AllOf GenSchemaList
HasAdditionalProperties bool
IsAdditionalProperties bool
AdditionalProperties *GenSchema
StrictAdditionalProperties bool
ReadOnly bool
IsVirtual bool
IsBaseType bool
HasBaseType bool
IsSubType bool
IsExported bool
DiscriminatorField string
DiscriminatorValue string
Discriminates map[string]string
Parents []string
IncludeValidator bool
IncludeModel bool
Default interface{}
WantsMarshalBinary bool // do we generate MarshalBinary interface?
StructTags []string
ExtraImports map[string]string // non-standard imports detected when using external types
ExternalDocs *spec.ExternalDocumentation
}
func (g GenSchema) renderMarshalTag() string {
if g.HasBaseType {
return "-"
}
var result strings.Builder
result.WriteString(g.OriginalName)
if !g.Required && g.IsEmptyOmitted {
result.WriteString(",omitempty")
}
if g.IsJSONString {
result.WriteString(",string")
}
return result.String()
}
// PrintTags takes care of rendering tags for a struct field
func (g GenSchema) PrintTags() string {
tags := make(map[string]string, 3)
orderedTags := make([]string, 0, 3)
tags["json"] = g.renderMarshalTag()
orderedTags = append(orderedTags, "json")
if len(g.XMLName) > 0 {
if !g.Required && g.IsEmptyOmitted {
tags["xml"] = g.XMLName + ",omitempty"
} else {
tags["xml"] = g.XMLName
}
orderedTags = append(orderedTags, "xml")
}
// Add extra struct tags, only if the tag hasn't already been set, i.e. example.
// Extra struct tags have the same value has the `json` tag.
for _, tag := range g.StructTags {
if _, exists := tags[tag]; exists {
// dedupe
continue
}
switch {
case tag == "example" && len(g.Example) > 0:
// only add example tag if it's contained in the struct tags
tags["example"] = g.Example // json representation of the example object
case tag == "description" && len(g.Description) > 0:
tags["description"] = g.Description
default:
tags[tag] = tags["json"]
}
orderedTags = append(orderedTags, tag)
}
// Assemble the tags in key value pairs with the value properly quoted.
kvPairs := make([]string, 0, len(orderedTags)+1)
for _, key := range orderedTags {
kvPairs = append(kvPairs, fmt.Sprintf("%s:%s", key, strconv.Quote(tags[key])))
}
if len(g.CustomTag) > 0 {
kvPairs = append(kvPairs, g.CustomTag)
}
// Join the key value pairs by a space.
completeTag := strings.Join(kvPairs, " ")
// If the values contain a backtick, we cannot render the tag using backticks because Go does not support
// escaping backticks in raw string literals.
valuesHaveBacktick := false
for _, value := range tags {
if !strconv.CanBackquote(value) {
valuesHaveBacktick = true
break
}
}
if !valuesHaveBacktick {
return fmt.Sprintf("`%s`", completeTag)
}
// We have to escape the tag again to put it in a literal with double quotes as the tag format uses double quotes.
return strconv.Quote(completeTag)
}
// UnderlyingType tells the go type or the aliased go type
func (g GenSchema) UnderlyingType() string {
if g.IsAliased {
return g.AliasedType
}
return g.GoType
}
// ToString returns a string conversion expression for the schema
func (g GenSchema) ToString() string {
return g.resolvedType.ToString(g.ValueExpression)
}
func (g GenSchemaList) Len() int { return len(g) }
func (g GenSchemaList) Swap(i, j int) { g[i], g[j] = g[j], g[i] }
func (g GenSchemaList) Less(i, j int) bool {
a, okA := g[i].Extensions[xOrder].(float64)
b, okB := g[j].Extensions[xOrder].(float64)
// If both properties have x-order defined, then the one with lower x-order is smaller
if okA && okB {
return a < b
}
// If only the first property has x-order defined, then it is smaller
if okA {
return true
}
// If only the second property has x-order defined, then it is smaller
if okB {
return false
}
// If neither property has x-order defined, then the one with lower lexicographic name is smaller
return g[i].Name < g[j].Name
}
type sharedValidations struct {
spec.SchemaValidations
HasValidations bool
HasContextValidations bool
Required bool
HasSliceValidations bool
ItemsEnum []interface{}
// NOTE: "patternProperties" and "dependencies" not supported by Swagger 2.0
}
// GenResponse represents a response object for code generation
type GenResponse struct {
Package string
ModelsPackage string
ReceiverName string
Name string
Description string
IsSuccess bool
Code int
Method string
Path string
Headers GenHeaders
Schema *GenSchema
AllowsForStreaming bool
Imports map[string]string
DefaultImports map[string]string
Extensions map[string]interface{}
StrictResponders bool
OperationName string
Examples GenResponseExamples
}
// GenResponseExamples is a sortable collection []GenResponseExample
type GenResponseExamples []GenResponseExample
func (g GenResponseExamples) Len() int { return len(g) }
func (g GenResponseExamples) Swap(i, j int) { g[i], g[j] = g[j], g[i] }
func (g GenResponseExamples) Less(i, j int) bool { return g[i].MediaType < g[j].MediaType }
// GenResponseExample captures an example provided for a response for some mime type
type GenResponseExample struct {
MediaType string
Example interface{}
}
// GenHeader represents a header on a response for code generation
type GenHeader struct {
resolvedType
sharedValidations
Package string
ReceiverName string
IndexVar string
ID string
Name string
Path string
ValueExpression string
Title string
Description string
Default interface{}
HasDefault bool
CollectionFormat string
Child *GenItems
Parent *GenItems
Converter string
Formatter string
ZeroValue string
}
// ItemsDepth returns a string "items.items..." with as many items as the level of nesting of the array.
// For a header objects it always returns "".
func (h *GenHeader) ItemsDepth() string {
// NOTE: this is currently used by templates to generate explicit comments in nested structures
return ""
}
// ToString returns a string conversion expression for the header
func (h GenHeader) ToString() string {
return h.resolvedType.ToString(h.ValueExpression)
}
// GenHeaders is a sorted collection of headers for codegen
type GenHeaders []GenHeader
func (g GenHeaders) Len() int { return len(g) }
func (g GenHeaders) Swap(i, j int) { g[i], g[j] = g[j], g[i] }
func (g GenHeaders) Less(i, j int) bool { return g[i].Name < g[j].Name }
// HasSomeDefaults returns true is at least one header has a default value set
func (g GenHeaders) HasSomeDefaults() bool {
// NOTE: this is currently used by templates to avoid empty constructs
for _, header := range g {
if header.HasDefault {
return true
}
}
return false
}
// GenParameter is used to represent
// a parameter or a header for code generation.
type GenParameter struct {
resolvedType
sharedValidations
ID string
Name string
ModelsPackage string
Path string
ValueExpression string
IndexVar string
KeyVar string
ReceiverName string
Location string
Title string
Description string
Converter string
Formatter string
Schema *GenSchema
CollectionFormat string
Child *GenItems
Parent *GenItems
// Unused
// BodyParam *GenParameter
Default interface{}
HasDefault bool
ZeroValue string
AllowEmptyValue bool
// validation strategy for Body params, which may mix model and simple constructs.
// Distinguish the following cases:
// - HasSimpleBodyParams: body is an inline simple type
// - HasModelBodyParams: body is a model objectd
// - HasSimpleBodyItems: body is an inline array of simple type
// - HasModelBodyItems: body is an array of model objects
// - HasSimpleBodyMap: body is a map of simple objects (possibly arrays)
// - HasModelBodyMap: body is a map of model objects
HasSimpleBodyParams bool
HasModelBodyParams bool
HasSimpleBodyItems bool
HasModelBodyItems bool
HasSimpleBodyMap bool
HasModelBodyMap bool
Extensions map[string]interface{}
}
// IsQueryParam returns true when this parameter is a query param
func (g *GenParameter) IsQueryParam() bool {
return g.Location == "query"
}
// IsPathParam returns true when this parameter is a path param
func (g *GenParameter) IsPathParam() bool {
return g.Location == "path"
}
// IsFormParam returns true when this parameter is a form param
func (g *GenParameter) IsFormParam() bool {
return g.Location == "formData"
}
// IsHeaderParam returns true when this parameter is a header param
func (g *GenParameter) IsHeaderParam() bool {
return g.Location == "header"
}
// IsBodyParam returns true when this parameter is a body param
func (g *GenParameter) IsBodyParam() bool {
return g.Location == "body"
}
// IsFileParam returns true when this parameter is a file param
func (g *GenParameter) IsFileParam() bool {
return g.SwaggerType == "file"
}
// ItemsDepth returns a string "items.items..." with as many items as the level of nesting of the array.
// For a parameter object, it always returns "".
func (g *GenParameter) ItemsDepth() string {
// NOTE: this is currently used by templates to generate explicit comments in nested structures
return ""
}
// UnderlyingType tells the go type or the aliased go type
func (g GenParameter) UnderlyingType() string {
return g.GoType
}
// ToString returns a string conversion expression for the parameter
func (g GenParameter) ToString() string {
return g.resolvedType.ToString(g.ValueExpression)
}
// GenParameters represents a sorted parameter collection
type GenParameters []GenParameter
func (g GenParameters) Len() int { return len(g) }
func (g GenParameters) Less(i, j int) bool { return g[i].Name < g[j].Name }
func (g GenParameters) Swap(i, j int) { g[i], g[j] = g[j], g[i] }
// HasSomeDefaults returns true is at least one parameter has a default value set
func (g GenParameters) HasSomeDefaults() bool {
// NOTE: this is currently used by templates to avoid empty constructs
for _, param := range g {
if param.HasDefault {
return true
}
}
return false
}
// GenItems represents the collection items for a collection parameter
type GenItems struct {
sharedValidations
resolvedType
Name string
Path string
ValueExpression string
CollectionFormat string
Child *GenItems
Parent *GenItems
Converter string
Formatter string
Location string
IndexVar string
KeyVar string
// instructs generator to skip the splitting and parsing from CollectionFormat
SkipParse bool
// instructs generator that some nested structure needs an higher level loop index
NeedsIndex bool
}
// ItemsDepth returns a string "items.items..." with as many items as the level of nesting of the array.
func (g *GenItems) ItemsDepth() string {
// NOTE: this is currently used by templates to generate explicit comments in nested structures
current := g
i := 1
for current.Parent != nil {
i++
current = current.Parent
}
return strings.Repeat("items.", i)
}
// UnderlyingType tells the go type or the aliased go type
func (g GenItems) UnderlyingType() string {
return g.GoType
}
// ToString returns a string conversion expression for the item
func (g GenItems) ToString() string {
return g.resolvedType.ToString(g.ValueExpression)
}
// GenOperationGroup represents a named (tagged) group of operations
type GenOperationGroup struct {
GenCommon
Name string
Operations GenOperations
Summary string
Description string
Imports map[string]string
DefaultImports map[string]string
RootPackage string
GenOpts *GenOpts
PackageAlias string
}
// GenOperationGroups is a sorted collection of operation groups
type GenOperationGroups []GenOperationGroup
func (g GenOperationGroups) Len() int { return len(g) }
func (g GenOperationGroups) Swap(i, j int) { g[i], g[j] = g[j], g[i] }
func (g GenOperationGroups) Less(i, j int) bool { return g[i].Name < g[j].Name }
// GenStatusCodeResponses a container for status code responses
type GenStatusCodeResponses []GenResponse
func (g GenStatusCodeResponses) Len() int { return len(g) }
func (g GenStatusCodeResponses) Swap(i, j int) { g[i], g[j] = g[j], g[i] }
func (g GenStatusCodeResponses) Less(i, j int) bool { return g[i].Code < g[j].Code }
// MarshalJSON marshals these responses to json
//
// This is used by DumpData.
func (g GenStatusCodeResponses) MarshalJSON() ([]byte, error) {
if g == nil {
return nil, nil
}
responses := make(GenStatusCodeResponses, len(g))
copy(responses, g)
// order marshalled output
sort.Sort(responses)
var buf bytes.Buffer
buf.WriteRune('{')
for i, v := range responses {
rb, err := json.Marshal(v)
if err != nil {
return nil, err
}
if i > 0 {
buf.WriteRune(',')
}
buf.WriteString(fmt.Sprintf("%q:", strconv.Itoa(v.Code)))
buf.Write(rb)
}
buf.WriteRune('}')
return buf.Bytes(), nil
}
// UnmarshalJSON unmarshals this GenStatusCodeResponses from json
func (g *GenStatusCodeResponses) UnmarshalJSON(data []byte) error {
var dd map[string]GenResponse
if err := json.Unmarshal(data, &dd); err != nil {
return err
}
var gg GenStatusCodeResponses
for _, v := range dd {
gg = append(gg, v)
}
sort.Sort(gg)
*g = gg
return nil
}
// GenOperation represents an operation for code generation
type GenOperation struct {
GenCommon
Package string
ReceiverName string
Name string
Summary string
Description string
Method string
Path string
BasePath string
Tags []string
UseTags bool
RootPackage string
Imports map[string]string
DefaultImports map[string]string
ExtraSchemas GenSchemaList
PackageAlias string
Authorized bool
Security []GenSecurityRequirements // resolved security requirements for the operation
SecurityDefinitions GenSecuritySchemes
SecurityRequirements []analysis.SecurityRequirement // original security requirements as per the spec (for doc)
Principal string
PrincipalIsNullable bool
SuccessResponse *GenResponse
SuccessResponses []GenResponse
Responses GenStatusCodeResponses
DefaultResponse *GenResponse
Params GenParameters
QueryParams GenParameters
PathParams GenParameters
HeaderParams GenParameters
FormParams GenParameters
HasQueryParams bool
HasPathParams bool
HasHeaderParams bool
HasFormParams bool
HasFormValueParams bool
HasFileParams bool
HasBodyParams bool
HasStreamingResponse bool
Schemes []string
ExtraSchemes []string
SchemeOverrides []string // original scheme overrides for operation, as per spec (for doc)
ExtraSchemeOverrides []string // original extra scheme overrides for operation, as per spec (for doc)
ProducesMediaTypes []string
ConsumesMediaTypes []string
TimeoutName string
Extensions map[string]interface{}
StrictResponders bool
ExternalDocs *spec.ExternalDocumentation
Produces []string // original produces for operation (for doc)
Consumes []string // original consumes for operation (for doc)
}
// GenOperations represents a list of operations to generate
// this implements a sort by operation id
type GenOperations []GenOperation
func (g GenOperations) Len() int { return len(g) }
func (g GenOperations) Less(i, j int) bool { return g[i].Name < g[j].Name }
func (g GenOperations) Swap(i, j int) { g[i], g[j] = g[j], g[i] }
// GenApp represents all the meta data needed to generate an application
// from a swagger spec
type GenApp struct {
GenCommon
APIPackage string
ServerPackageAlias string
ImplementationPackageAlias string
APIPackageAlias string
Package string
ReceiverName string
Name string
Principal string
PrincipalIsNullable bool
DefaultConsumes string
DefaultProduces string
Host string
BasePath string
Info *spec.Info
ExternalDocs *spec.ExternalDocumentation
Tags []spec.Tag
Imports map[string]string
DefaultImports map[string]string
Schemes []string
ExtraSchemes []string
Consumes GenSerGroups
Produces GenSerGroups
SecurityDefinitions GenSecuritySchemes
SecurityRequirements []analysis.SecurityRequirement // original security requirements as per the spec (for doc)
Models []GenDefinition
Operations GenOperations
OperationGroups GenOperationGroups
SwaggerJSON string
// Embedded specs: this is important for when the generated server adds routes.
// NOTE: there is a distinct advantage to having this in runtime rather than generated code.
// We are not ever going to generate the router.
// If embedding spec is an issue (e.g. memory usage), this can be excluded with the --exclude-spec
// generation option. Alternative methods to serve spec (e.g. from disk, ...) may be implemented by
// adding a middleware to the generated API.
FlatSwaggerJSON string
ExcludeSpec bool
GenOpts *GenOpts
}
// UseGoStructFlags returns true when no strategy is specified or it is set to "go-flags"
func (g *GenApp) UseGoStructFlags() bool {
if g.GenOpts == nil {
return true
}
return g.GenOpts.FlagStrategy == "" || g.GenOpts.FlagStrategy == "go-flags"
}
// UsePFlags returns true when the flag strategy is set to pflag
func (g *GenApp) UsePFlags() bool {
return g.GenOpts != nil && strings.HasPrefix(g.GenOpts.FlagStrategy, "pflag")
}
// UseFlags returns true when the flag strategy is set to flag
func (g *GenApp) UseFlags() bool {
return g.GenOpts != nil && strings.HasPrefix(g.GenOpts.FlagStrategy, "flag")
}
// UseIntermediateMode for https://wiki.mozilla.org/Security/Server_Side_TLS#Intermediate_compatibility_.28default.29
func (g *GenApp) UseIntermediateMode() bool {
return g.GenOpts != nil && g.GenOpts.CompatibilityMode == "intermediate"
}
// UseModernMode for https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility
func (g *GenApp) UseModernMode() bool {
return g.GenOpts == nil || g.GenOpts.CompatibilityMode == "" || g.GenOpts.CompatibilityMode == "modern"
}
// GenSerGroups sorted representation of serializer groups
type GenSerGroups []GenSerGroup
func (g GenSerGroups) Len() int { return len(g) }
func (g GenSerGroups) Swap(i, j int) { g[i], g[j] = g[j], g[i] }
func (g GenSerGroups) Less(i, j int) bool { return g[i].Name < g[j].Name }
// GenSerGroup represents a group of serializers: this links a serializer to a list of
// prioritized media types (mime).
type GenSerGroup struct {
GenSerializer
// All media types for this serializer. The redundant representation allows for easier use in templates
AllSerializers GenSerializers
}
// GenSerializers sorted representation of serializers
type GenSerializers []GenSerializer
func (g GenSerializers) Len() int { return len(g) }
func (g GenSerializers) Swap(i, j int) { g[i], g[j] = g[j], g[i] }
func (g GenSerializers) Less(i, j int) bool { return g[i].MediaType < g[j].MediaType }
// GenSerializer represents a single serializer for a particular media type
type GenSerializer struct {
AppName string // Application name
ReceiverName string
Name string // Name of the Producer/Consumer (e.g. json, yaml, txt, bin)
MediaType string // mime
Implementation string // func implementing the Producer/Consumer
Parameters []string // parameters supported by this serializer
}
// GenSecurityScheme represents a security scheme for code generation
type GenSecurityScheme struct {
AppName string
ID string
Name string
ReceiverName string
IsBasicAuth bool
IsAPIKeyAuth bool
IsOAuth2 bool
Scopes []string
Source string
Principal string
PrincipalIsNullable bool
// from spec.SecurityScheme
Description string
Type string
In string
Flow string
AuthorizationURL string
TokenURL string
Extensions map[string]interface{}
ScopesDesc []GenSecurityScope
}
// GenSecuritySchemes sorted representation of serializers
type GenSecuritySchemes []GenSecurityScheme
func (g GenSecuritySchemes) Len() int { return len(g) }
func (g GenSecuritySchemes) Swap(i, j int) { g[i], g[j] = g[j], g[i] }
func (g GenSecuritySchemes) Less(i, j int) bool { return g[i].ID < g[j].ID }
// GenSecurityRequirement represents a security requirement for an operation
type GenSecurityRequirement struct {
Name string
Scopes []string
}
// GenSecurityScope represents a scope descriptor for an OAuth2 security scheme
type GenSecurityScope struct {
Name string
Description string
}
// GenSecurityRequirements represents a compounded security requirement specification.
// In a []GenSecurityRequirements complete requirements specification,
// outer elements are interpreted as optional requirements (OR), and
// inner elements are interpreted as jointly required (AND).
type GenSecurityRequirements []GenSecurityRequirement
func (g GenSecurityRequirements) Len() int { return len(g) }
func (g GenSecurityRequirements) Swap(i, j int) { g[i], g[j] = g[j], g[i] }
func (g GenSecurityRequirements) Less(i, j int) bool { return g[i].Name < g[j].Name }

View file

@ -0,0 +1,546 @@
// 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 generator
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"log"
"path"
"path/filepath"
"sort"
"github.com/go-openapi/analysis"
"github.com/go-openapi/loads"
"github.com/go-openapi/spec"
"github.com/go-openapi/swag"
)
// GenerateServer generates a server application
func GenerateServer(name string, modelNames, operationIDs []string, opts *GenOpts) error {
generator, err := newAppGenerator(name, modelNames, operationIDs, opts)
if err != nil {
return err
}
return generator.Generate()
}
// GenerateSupport generates the supporting files for an API
func GenerateSupport(name string, modelNames, operationIDs []string, opts *GenOpts) error {
generator, err := newAppGenerator(name, modelNames, operationIDs, opts)
if err != nil {
return err
}
return generator.GenerateSupport(nil)
}
// GenerateMarkdown documentation for a swagger specification
func GenerateMarkdown(output string, modelNames, operationIDs []string, opts *GenOpts) error {
if output == "." || output == "" {
output = "markdown.md"
}
if err := opts.EnsureDefaults(); err != nil {
return err
}
MarkdownSectionOpts(opts, output)
generator, err := newAppGenerator("", modelNames, operationIDs, opts)
if err != nil {
return err
}
return generator.GenerateMarkdown()
}
func newAppGenerator(name string, modelNames, operationIDs []string, opts *GenOpts) (*appGenerator, error) {
if err := opts.CheckOpts(); err != nil {
return nil, err
}
if err := opts.setTemplates(); err != nil {
return nil, err
}
specDoc, analyzed, err := opts.analyzeSpec()
if err != nil {
return nil, err
}
models, err := gatherModels(specDoc, modelNames)
if err != nil {
return nil, err
}
operations := gatherOperations(analyzed, operationIDs)
if len(operations) == 0 && !opts.IgnoreOperations {
return nil, errors.New("no operations were selected")
}
opts.Name = appNameOrDefault(specDoc, name, defaultServerName)
if opts.IncludeMain && opts.MainPackage == "" {
// default target for the generated main
opts.MainPackage = swag.ToCommandName(mainNameOrDefault(specDoc, name, defaultServerName) + "-server")
}
apiPackage := opts.LanguageOpts.ManglePackagePath(opts.APIPackage, defaultOperationsTarget)
return &appGenerator{
Name: opts.Name,
Receiver: "o",
SpecDoc: specDoc,
Analyzed: analyzed,
Models: models,
Operations: operations,
Target: opts.Target,
DumpData: opts.DumpData,
Package: opts.LanguageOpts.ManglePackageName(apiPackage, defaultOperationsTarget),
APIPackage: apiPackage,
ModelsPackage: opts.LanguageOpts.ManglePackagePath(opts.ModelPackage, defaultModelsTarget),
ServerPackage: opts.LanguageOpts.ManglePackagePath(opts.ServerPackage, defaultServerTarget),
ClientPackage: opts.LanguageOpts.ManglePackagePath(opts.ClientPackage, defaultClientTarget),
OperationsPackage: filepath.Join(opts.LanguageOpts.ManglePackagePath(opts.ServerPackage, defaultServerTarget), apiPackage),
Principal: opts.PrincipalAlias(),
DefaultScheme: opts.DefaultScheme,
DefaultProduces: opts.DefaultProduces,
DefaultConsumes: opts.DefaultConsumes,
GenOpts: opts,
}, nil
}
type appGenerator struct {
Name string
Receiver string
SpecDoc *loads.Document
Analyzed *analysis.Spec
Package string
APIPackage string
ModelsPackage string
ServerPackage string
ClientPackage string
OperationsPackage string
MainPackage string
Principal string
Models map[string]spec.Schema
Operations map[string]opRef
Target string
DumpData bool
DefaultScheme string
DefaultProduces string
DefaultConsumes string
GenOpts *GenOpts
}
func (a *appGenerator) Generate() error {
app, err := a.makeCodegenApp()
if err != nil {
return err
}
if a.DumpData {
return dumpData(app)
}
// NOTE: relative to previous implem with chan.
// IPC removed concurrent execution because of the FuncMap that is being shared
// templates are now lazy loaded so there is concurrent map access I can't guard
if a.GenOpts.IncludeModel {
log.Printf("rendering %d models", len(app.Models))
for _, md := range app.Models {
mod := md
mod.IncludeModel = true
mod.IncludeValidator = a.GenOpts.IncludeValidator
if err := a.GenOpts.renderDefinition(&mod); err != nil {
return err
}
}
}
if a.GenOpts.IncludeHandler {
log.Printf("rendering %d operation groups (tags)", app.OperationGroups.Len())
for _, g := range app.OperationGroups {
opg := g
log.Printf("rendering %d operations for %s", opg.Operations.Len(), opg.Name)
for _, p := range opg.Operations {
op := p
if err := a.GenOpts.renderOperation(&op); err != nil {
return err
}
}
// optional OperationGroups templates generation
if err := a.GenOpts.renderOperationGroup(&opg); err != nil {
return fmt.Errorf("error while rendering operation group: %v", err)
}
}
}
if a.GenOpts.IncludeSupport {
log.Printf("rendering support")
if err := a.GenerateSupport(&app); err != nil {
return err
}
}
return nil
}
func (a *appGenerator) GenerateSupport(ap *GenApp) error {
app := ap
if ap == nil {
// allows for calling GenerateSupport standalone
ca, err := a.makeCodegenApp()
if err != nil {
return err
}
app = &ca
}
baseImport := a.GenOpts.LanguageOpts.baseImport(a.Target)
serverPath := path.Join(baseImport,
a.GenOpts.LanguageOpts.ManglePackagePath(a.ServerPackage, defaultServerTarget))
pkgAlias := deconflictPkg(importAlias(serverPath), renameServerPackage)
app.DefaultImports[pkgAlias] = serverPath
app.ServerPackageAlias = pkgAlias
// add client import for cli generation
clientPath := path.Join(baseImport,
a.GenOpts.LanguageOpts.ManglePackagePath(a.ClientPackage, defaultClientTarget))
clientPkgAlias := importAlias(clientPath)
app.DefaultImports[clientPkgAlias] = clientPath
return a.GenOpts.renderApplication(app)
}
func (a *appGenerator) GenerateMarkdown() error {
app, err := a.makeCodegenApp()
if err != nil {
return err
}
return a.GenOpts.renderApplication(&app)
}
func (a *appGenerator) makeSecuritySchemes() GenSecuritySchemes {
requiredSecuritySchemes := make(map[string]spec.SecurityScheme, len(a.Analyzed.RequiredSecuritySchemes()))
for _, scheme := range a.Analyzed.RequiredSecuritySchemes() {
if req, ok := a.SpecDoc.Spec().SecurityDefinitions[scheme]; ok && req != nil {
requiredSecuritySchemes[scheme] = *req
}
}
return gatherSecuritySchemes(requiredSecuritySchemes, a.Name, a.Principal, a.Receiver, a.GenOpts.PrincipalIsNullable())
}
func (a *appGenerator) makeCodegenApp() (GenApp, error) {
log.Println("building a plan for generation")
sw := a.SpecDoc.Spec()
receiver := a.Receiver
consumes, _ := a.makeConsumes()
produces, _ := a.makeProduces()
security := a.makeSecuritySchemes()
log.Println("generation target", a.Target)
baseImport := a.GenOpts.LanguageOpts.baseImport(a.Target)
defaultImports := a.GenOpts.defaultImports()
imports := make(map[string]string, 50)
alias := deconflictPkg(a.GenOpts.LanguageOpts.ManglePackageName(a.OperationsPackage, defaultOperationsTarget), renameAPIPackage)
imports[alias] = path.Join(
baseImport,
a.GenOpts.LanguageOpts.ManglePackagePath(a.OperationsPackage, defaultOperationsTarget))
implAlias := ""
if a.GenOpts.ImplementationPackage != "" {
implAlias = deconflictPkg(a.GenOpts.LanguageOpts.ManglePackageName(a.GenOpts.ImplementationPackage, defaultImplementationTarget), renameImplementationPackage)
imports[implAlias] = a.GenOpts.ImplementationPackage
}
log.Printf("planning definitions (found: %d)", len(a.Models))
genModels := make(GenDefinitions, 0, len(a.Models))
for mn, m := range a.Models {
model, err := makeGenDefinition(
mn,
a.ModelsPackage,
m,
a.SpecDoc,
a.GenOpts,
)
if err != nil {
return GenApp{}, fmt.Errorf("error in model %s while planning definitions: %v", mn, err)
}
if model != nil {
if !model.External {
genModels = append(genModels, *model)
}
// Copy model imports to operation imports
// TODO(fredbi): mangle model pkg aliases
for alias, pkg := range model.Imports {
target := a.GenOpts.LanguageOpts.ManglePackageName(alias, "")
imports[target] = pkg
}
}
}
sort.Sort(genModels)
log.Printf("planning operations (found: %d)", len(a.Operations))
genOps := make(GenOperations, 0, len(a.Operations))
for operationName, opp := range a.Operations {
o := opp.Op
o.ID = operationName
bldr := codeGenOpBuilder{
ModelsPackage: a.ModelsPackage,
Principal: a.GenOpts.PrincipalAlias(),
Target: a.Target,
DefaultImports: defaultImports,
Imports: imports,
DefaultScheme: a.DefaultScheme,
Doc: a.SpecDoc,
Analyzed: a.Analyzed,
BasePath: a.SpecDoc.BasePath(),
GenOpts: a.GenOpts,
Name: operationName,
Operation: *o,
Method: opp.Method,
Path: opp.Path,
IncludeValidator: a.GenOpts.IncludeValidator,
APIPackage: a.APIPackage, // defaults to main operations package
DefaultProduces: a.DefaultProduces,
DefaultConsumes: a.DefaultConsumes,
}
tag, tags, ok := bldr.analyzeTags()
if !ok {
continue // operation filtered according to CLI params
}
bldr.Authed = len(a.Analyzed.SecurityRequirementsFor(o)) > 0
bldr.Security = a.Analyzed.SecurityRequirementsFor(o)
bldr.SecurityDefinitions = a.Analyzed.SecurityDefinitionsFor(o)
bldr.RootAPIPackage = a.GenOpts.LanguageOpts.ManglePackageName(a.ServerPackage, defaultServerTarget)
st := o.Tags
if a.GenOpts != nil {
st = a.GenOpts.Tags
}
intersected := intersectTags(o.Tags, st)
if len(st) > 0 && len(intersected) == 0 {
continue
}
op, err := bldr.MakeOperation()
if err != nil {
return GenApp{}, err
}
op.ReceiverName = receiver
op.Tags = tags // ordered tags for this operation, possibly filtered by CLI params
genOps = append(genOps, op)
if !a.GenOpts.SkipTagPackages && tag != "" {
importPath := filepath.ToSlash(
path.Join(
baseImport,
a.GenOpts.LanguageOpts.ManglePackagePath(a.OperationsPackage, defaultOperationsTarget),
a.GenOpts.LanguageOpts.ManglePackageName(bldr.APIPackage, defaultOperationsTarget),
))
defaultImports[bldr.APIPackageAlias] = importPath
}
}
sort.Sort(genOps)
opsGroupedByPackage := make(map[string]GenOperations, len(genOps))
for _, operation := range genOps {
opsGroupedByPackage[operation.PackageAlias] = append(opsGroupedByPackage[operation.PackageAlias], operation)
}
log.Printf("grouping operations into packages (packages: %d)", len(opsGroupedByPackage))
opGroups := make(GenOperationGroups, 0, len(opsGroupedByPackage))
for k, v := range opsGroupedByPackage {
log.Printf("operations for package packages %q (found: %d)", k, len(v))
sort.Sort(v)
// trim duplicate extra schemas within the same package
vv := make(GenOperations, 0, len(v))
seenExtraSchema := make(map[string]bool)
for _, op := range v {
uniqueExtraSchemas := make(GenSchemaList, 0, len(op.ExtraSchemas))
for _, xs := range op.ExtraSchemas {
if _, alreadyThere := seenExtraSchema[xs.Name]; !alreadyThere {
seenExtraSchema[xs.Name] = true
uniqueExtraSchemas = append(uniqueExtraSchemas, xs)
}
}
op.ExtraSchemas = uniqueExtraSchemas
vv = append(vv, op)
}
var pkg string
if len(vv) > 0 {
pkg = vv[0].Package
} else {
pkg = k
}
opGroup := GenOperationGroup{
GenCommon: GenCommon{
Copyright: a.GenOpts.Copyright,
TargetImportPath: baseImport,
},
Name: pkg,
PackageAlias: k,
Operations: vv,
DefaultImports: defaultImports,
Imports: imports,
RootPackage: a.APIPackage,
GenOpts: a.GenOpts,
}
opGroups = append(opGroups, opGroup)
}
sort.Sort(opGroups)
log.Println("planning meta data and facades")
var collectedSchemes, extraSchemes []string
for _, op := range genOps {
collectedSchemes = concatUnique(collectedSchemes, op.Schemes)
extraSchemes = concatUnique(extraSchemes, op.ExtraSchemes)
}
sort.Strings(collectedSchemes)
sort.Strings(extraSchemes)
host := "localhost"
if sw.Host != "" {
host = sw.Host
}
basePath := "/"
if sw.BasePath != "" {
basePath = sw.BasePath
}
jsonb, _ := json.MarshalIndent(a.SpecDoc.OrigSpec(), "", " ")
flatjsonb, _ := json.MarshalIndent(a.SpecDoc.Spec(), "", " ")
return GenApp{
GenCommon: GenCommon{
Copyright: a.GenOpts.Copyright,
TargetImportPath: baseImport,
},
APIPackage: a.GenOpts.LanguageOpts.ManglePackageName(a.ServerPackage, defaultServerTarget),
APIPackageAlias: alias,
ImplementationPackageAlias: implAlias,
Package: a.Package,
ReceiverName: receiver,
Name: a.Name,
Host: host,
BasePath: basePath,
Schemes: schemeOrDefault(collectedSchemes, a.DefaultScheme),
ExtraSchemes: extraSchemes,
ExternalDocs: trimExternalDoc(sw.ExternalDocs),
Tags: trimTags(sw.Tags),
Info: trimInfo(sw.Info),
Consumes: consumes,
Produces: produces,
DefaultConsumes: a.DefaultConsumes,
DefaultProduces: a.DefaultProduces,
DefaultImports: defaultImports,
Imports: imports,
SecurityDefinitions: security,
SecurityRequirements: securityRequirements(a.SpecDoc.Spec().Security), // top level securityRequirements
Models: genModels,
Operations: genOps,
OperationGroups: opGroups,
Principal: a.GenOpts.PrincipalAlias(),
SwaggerJSON: generateReadableSpec(jsonb),
FlatSwaggerJSON: generateReadableSpec(flatjsonb),
ExcludeSpec: a.GenOpts.ExcludeSpec,
GenOpts: a.GenOpts,
PrincipalIsNullable: a.GenOpts.PrincipalIsNullable(),
}, nil
}
// generateReadableSpec makes swagger json spec as a string instead of bytes
// the only character that needs to be escaped is '`' symbol, since it cannot be escaped in the GO string
// that is quoted as `string data`. The function doesn't care about the beginning or the ending of the
// string it escapes since all data that needs to be escaped is always in the middle of the swagger spec.
func generateReadableSpec(spec []byte) string {
buf := &bytes.Buffer{}
for _, b := range string(spec) {
if b == '`' {
buf.WriteString("`+\"`\"+`")
} else {
buf.WriteRune(b)
}
}
return buf.String()
}
func trimExternalDoc(in *spec.ExternalDocumentation) *spec.ExternalDocumentation {
if in == nil {
return nil
}
return &spec.ExternalDocumentation{
URL: in.URL,
Description: trimBOM(in.Description),
}
}
func trimInfo(in *spec.Info) *spec.Info {
if in == nil {
return nil
}
return &spec.Info{
InfoProps: spec.InfoProps{
Contact: in.Contact,
Title: trimBOM(in.Title),
Description: trimBOM(in.Description),
TermsOfService: trimBOM(in.TermsOfService),
License: in.License,
Version: in.Version,
},
VendorExtensible: in.VendorExtensible,
}
}
func trimTags(in []spec.Tag) []spec.Tag {
if in == nil {
return nil
}
tags := make([]spec.Tag, 0, len(in))
for _, tag := range in {
tags = append(tags, spec.Tag{
TagProps: spec.TagProps{
Name: tag.Name,
Description: trimBOM(tag.Description),
ExternalDocs: trimExternalDoc(tag.ExternalDocs),
},
})
}
return tags
}

View file

@ -0,0 +1,855 @@
package generator
import (
"bytes"
"encoding/json"
"fmt"
"math"
"os"
"path"
"path/filepath"
"reflect"
"strconv"
"strings"
"sync"
"text/template"
"text/template/parse"
"unicode"
"log"
"github.com/Masterminds/sprig/v3"
"github.com/go-openapi/inflect"
"github.com/go-openapi/runtime"
"github.com/go-openapi/swag"
"github.com/kr/pretty"
)
var (
assets map[string][]byte
protectedTemplates map[string]bool
// FuncMapFunc yields a map with all functions for templates
FuncMapFunc func(*LanguageOpts) template.FuncMap
templates *Repository
docFormat map[string]string
)
func initTemplateRepo() {
FuncMapFunc = DefaultFuncMap
// this makes the ToGoName func behave with the special
// prefixing rule above
swag.GoNamePrefixFunc = prefixForName
assets = defaultAssets()
protectedTemplates = defaultProtectedTemplates()
templates = NewRepository(FuncMapFunc(DefaultLanguageFunc()))
docFormat = map[string]string{
"binary": "binary (byte stream)",
"byte": "byte (base64 string)",
}
}
// DefaultFuncMap yields a map with default functions for use in the templates.
// These are available in every template
func DefaultFuncMap(lang *LanguageOpts) template.FuncMap {
f := sprig.TxtFuncMap()
extra := template.FuncMap{
"pascalize": pascalize,
"camelize": swag.ToJSONName,
"varname": lang.MangleVarName,
"humanize": swag.ToHumanNameLower,
"snakize": lang.MangleFileName,
"toPackagePath": func(name string) string {
return filepath.FromSlash(lang.ManglePackagePath(name, ""))
},
"toPackage": func(name string) string {
return lang.ManglePackagePath(name, "")
},
"toPackageName": func(name string) string {
return lang.ManglePackageName(name, "")
},
"dasherize": swag.ToCommandName,
"pluralizeFirstWord": pluralizeFirstWord,
"json": asJSON,
"prettyjson": asPrettyJSON,
"hasInsecure": func(arg []string) bool {
return swag.ContainsStringsCI(arg, "http") || swag.ContainsStringsCI(arg, "ws")
},
"hasSecure": func(arg []string) bool {
return swag.ContainsStringsCI(arg, "https") || swag.ContainsStringsCI(arg, "wss")
},
"dropPackage": dropPackage,
"containsPkgStr": containsPkgStr,
"contains": swag.ContainsStrings,
"padSurround": padSurround,
"joinFilePath": filepath.Join,
"joinPath": path.Join,
"comment": padComment,
"blockcomment": blockComment,
"inspect": pretty.Sprint,
"cleanPath": path.Clean,
"mediaTypeName": mediaMime,
"arrayInitializer": lang.arrayInitializer,
"hasPrefix": strings.HasPrefix,
"stringContains": strings.Contains,
"imports": lang.imports,
"dict": dict,
"isInteger": isInteger,
"escapeBackticks": func(arg string) string {
return strings.ReplaceAll(arg, "`", "`+\"`\"+`")
},
"paramDocType": func(param GenParameter) string {
return resolvedDocType(param.SwaggerType, param.SwaggerFormat, param.Child)
},
"headerDocType": func(header GenHeader) string {
return resolvedDocType(header.SwaggerType, header.SwaggerFormat, header.Child)
},
"schemaDocType": func(in interface{}) string {
switch schema := in.(type) {
case GenSchema:
return resolvedDocSchemaType(schema.SwaggerType, schema.SwaggerFormat, schema.Items)
case *GenSchema:
if schema == nil {
return ""
}
return resolvedDocSchemaType(schema.SwaggerType, schema.SwaggerFormat, schema.Items)
case GenDefinition:
return resolvedDocSchemaType(schema.SwaggerType, schema.SwaggerFormat, schema.Items)
case *GenDefinition:
if schema == nil {
return ""
}
return resolvedDocSchemaType(schema.SwaggerType, schema.SwaggerFormat, schema.Items)
default:
panic("dev error: schemaDocType should be called with GenSchema or GenDefinition")
}
},
"schemaDocMapType": func(schema GenSchema) string {
return resolvedDocElemType("object", schema.SwaggerFormat, &schema.resolvedType)
},
"docCollectionFormat": resolvedDocCollectionFormat,
"trimSpace": strings.TrimSpace,
"httpStatus": httpStatus,
"cleanupEnumVariant": cleanupEnumVariant,
"gt0": gt0,
}
for k, v := range extra {
f[k] = v
}
return f
}
func defaultAssets() map[string][]byte {
return map[string][]byte{
// schema validation templates
"validation/primitive.gotmpl": MustAsset("templates/validation/primitive.gotmpl"),
"validation/customformat.gotmpl": MustAsset("templates/validation/customformat.gotmpl"),
"validation/structfield.gotmpl": MustAsset("templates/validation/structfield.gotmpl"),
"structfield.gotmpl": MustAsset("templates/structfield.gotmpl"),
"schemavalidator.gotmpl": MustAsset("templates/schemavalidator.gotmpl"),
"schemapolymorphic.gotmpl": MustAsset("templates/schemapolymorphic.gotmpl"),
"schemaembedded.gotmpl": MustAsset("templates/schemaembedded.gotmpl"),
"validation/minimum.gotmpl": MustAsset("templates/validation/minimum.gotmpl"),
"validation/maximum.gotmpl": MustAsset("templates/validation/maximum.gotmpl"),
"validation/multipleOf.gotmpl": MustAsset("templates/validation/multipleOf.gotmpl"),
// schema serialization templates
"additionalpropertiesserializer.gotmpl": MustAsset("templates/serializers/additionalpropertiesserializer.gotmpl"),
"aliasedserializer.gotmpl": MustAsset("templates/serializers/aliasedserializer.gotmpl"),
"allofserializer.gotmpl": MustAsset("templates/serializers/allofserializer.gotmpl"),
"basetypeserializer.gotmpl": MustAsset("templates/serializers/basetypeserializer.gotmpl"),
"marshalbinaryserializer.gotmpl": MustAsset("templates/serializers/marshalbinaryserializer.gotmpl"),
"schemaserializer.gotmpl": MustAsset("templates/serializers/schemaserializer.gotmpl"),
"subtypeserializer.gotmpl": MustAsset("templates/serializers/subtypeserializer.gotmpl"),
"tupleserializer.gotmpl": MustAsset("templates/serializers/tupleserializer.gotmpl"),
// schema generation template
"docstring.gotmpl": MustAsset("templates/docstring.gotmpl"),
"schematype.gotmpl": MustAsset("templates/schematype.gotmpl"),
"schemabody.gotmpl": MustAsset("templates/schemabody.gotmpl"),
"schema.gotmpl": MustAsset("templates/schema.gotmpl"),
"model.gotmpl": MustAsset("templates/model.gotmpl"),
"header.gotmpl": MustAsset("templates/header.gotmpl"),
// simple schema generation helpers templates
"simpleschema/defaultsvar.gotmpl": MustAsset("templates/simpleschema/defaultsvar.gotmpl"),
"simpleschema/defaultsinit.gotmpl": MustAsset("templates/simpleschema/defaultsinit.gotmpl"),
"swagger_json_embed.gotmpl": MustAsset("templates/swagger_json_embed.gotmpl"),
// server templates
"server/parameter.gotmpl": MustAsset("templates/server/parameter.gotmpl"),
"server/urlbuilder.gotmpl": MustAsset("templates/server/urlbuilder.gotmpl"),
"server/responses.gotmpl": MustAsset("templates/server/responses.gotmpl"),
"server/operation.gotmpl": MustAsset("templates/server/operation.gotmpl"),
"server/builder.gotmpl": MustAsset("templates/server/builder.gotmpl"),
"server/server.gotmpl": MustAsset("templates/server/server.gotmpl"),
"server/configureapi.gotmpl": MustAsset("templates/server/configureapi.gotmpl"),
"server/autoconfigureapi.gotmpl": MustAsset("templates/server/autoconfigureapi.gotmpl"),
"server/main.gotmpl": MustAsset("templates/server/main.gotmpl"),
"server/doc.gotmpl": MustAsset("templates/server/doc.gotmpl"),
// client templates
"client/parameter.gotmpl": MustAsset("templates/client/parameter.gotmpl"),
"client/response.gotmpl": MustAsset("templates/client/response.gotmpl"),
"client/client.gotmpl": MustAsset("templates/client/client.gotmpl"),
"client/facade.gotmpl": MustAsset("templates/client/facade.gotmpl"),
"markdown/docs.gotmpl": MustAsset("templates/markdown/docs.gotmpl"),
// cli templates
"cli/cli.gotmpl": MustAsset("templates/cli/cli.gotmpl"),
"cli/main.gotmpl": MustAsset("templates/cli/main.gotmpl"),
"cli/modelcli.gotmpl": MustAsset("templates/cli/modelcli.gotmpl"),
"cli/operation.gotmpl": MustAsset("templates/cli/operation.gotmpl"),
"cli/registerflag.gotmpl": MustAsset("templates/cli/registerflag.gotmpl"),
"cli/retrieveflag.gotmpl": MustAsset("templates/cli/retrieveflag.gotmpl"),
"cli/schema.gotmpl": MustAsset("templates/cli/schema.gotmpl"),
"cli/completion.gotmpl": MustAsset("templates/cli/completion.gotmpl"),
}
}
func defaultProtectedTemplates() map[string]bool {
return map[string]bool{
"dereffedSchemaType": true,
"docstring": true,
"header": true,
"mapvalidator": true,
"model": true,
"modelvalidator": true,
"objectvalidator": true,
"primitivefieldvalidator": true,
"privstructfield": true,
"privtuplefield": true,
"propertyValidationDocString": true,
"propertyvalidator": true,
"schema": true,
"schemaBody": true,
"schemaType": true,
"schemabody": true,
"schematype": true,
"schemavalidator": true,
"serverDoc": true,
"slicevalidator": true,
"structfield": true,
"structfieldIface": true,
"subTypeBody": true,
"swaggerJsonEmbed": true,
"tuplefield": true,
"tuplefieldIface": true,
"typeSchemaType": true,
"simpleschemaDefaultsvar": true,
"simpleschemaDefaultsinit": true,
// validation helpers
"validationCustomformat": true,
"validationPrimitive": true,
"validationStructfield": true,
"withBaseTypeBody": true,
"withoutBaseTypeBody": true,
"validationMinimum": true,
"validationMaximum": true,
"validationMultipleOf": true,
// all serializers
"additionalPropertiesSerializer": true,
"tupleSerializer": true,
"schemaSerializer": true,
"hasDiscriminatedSerializer": true,
"discriminatedSerializer": true,
}
}
// AddFile adds a file to the default repository. It will create a new template based on the filename.
// It trims the .gotmpl from the end and converts the name using swag.ToJSONName. This will strip
// directory separators and Camelcase the next letter.
// e.g validation/primitive.gotmpl will become validationPrimitive
//
// If the file contains a definition for a template that is protected the whole file will not be added
func AddFile(name, data string) error {
return templates.addFile(name, data, false)
}
// NewRepository creates a new template repository with the provided functions defined
func NewRepository(funcs template.FuncMap) *Repository {
repo := Repository{
files: make(map[string]string),
templates: make(map[string]*template.Template),
funcs: funcs,
}
if repo.funcs == nil {
repo.funcs = make(template.FuncMap)
}
return &repo
}
// Repository is the repository for the generator templates
type Repository struct {
files map[string]string
templates map[string]*template.Template
funcs template.FuncMap
allowOverride bool
mux sync.Mutex
}
// ShallowClone a repository.
//
// Clones the maps of files and templates, so as to be able to use
// the cloned repo concurrently.
func (t *Repository) ShallowClone() *Repository {
clone := &Repository{
files: make(map[string]string, len(t.files)),
templates: make(map[string]*template.Template, len(t.templates)),
funcs: t.funcs,
allowOverride: t.allowOverride,
}
t.mux.Lock()
defer t.mux.Unlock()
for k, file := range t.files {
clone.files[k] = file
}
for k, tpl := range t.templates {
clone.templates[k] = tpl
}
return clone
}
// LoadDefaults will load the embedded templates
func (t *Repository) LoadDefaults() {
for name, asset := range assets {
if err := t.addFile(name, string(asset), true); err != nil {
log.Fatal(err)
}
}
}
// LoadDir will walk the specified path and add each .gotmpl file it finds to the repository
func (t *Repository) LoadDir(templatePath string) error {
err := filepath.Walk(templatePath, func(path string, info os.FileInfo, err error) error {
if strings.HasSuffix(path, ".gotmpl") {
if assetName, e := filepath.Rel(templatePath, path); e == nil {
if data, e := os.ReadFile(path); e == nil {
if ee := t.AddFile(assetName, string(data)); ee != nil {
return fmt.Errorf("could not add template: %v", ee)
}
}
// Non-readable files are skipped
}
}
if err != nil {
return err
}
// Non-template files are skipped
return nil
})
if err != nil {
return fmt.Errorf("could not complete template processing in directory \"%s\": %v", templatePath, err)
}
return nil
}
// LoadContrib loads template from contrib directory
func (t *Repository) LoadContrib(name string) error {
log.Printf("loading contrib %s", name)
const pathPrefix = "templates/contrib/"
basePath := pathPrefix + name
filesAdded := 0
for _, aname := range AssetNames() {
if !strings.HasSuffix(aname, ".gotmpl") {
continue
}
if strings.HasPrefix(aname, basePath) {
target := aname[len(basePath)+1:]
err := t.addFile(target, string(MustAsset(aname)), true)
if err != nil {
return err
}
log.Printf("added contributed template %s from %s", target, aname)
filesAdded++
}
}
if filesAdded == 0 {
return fmt.Errorf("no files added from template: %s", name)
}
return nil
}
func (t *Repository) addFile(name, data string, allowOverride bool) error {
fileName := name
name = swag.ToJSONName(strings.TrimSuffix(name, ".gotmpl"))
templ, err := template.New(name).Funcs(t.funcs).Parse(data)
if err != nil {
return fmt.Errorf("failed to load template %s: %v", name, err)
}
// check if any protected templates are defined
if !allowOverride && !t.allowOverride {
for _, template := range templ.Templates() {
if protectedTemplates[template.Name()] {
return fmt.Errorf("cannot overwrite protected template %s", template.Name())
}
}
}
// Add each defined template into the cache
for _, template := range templ.Templates() {
t.files[template.Name()] = fileName
t.templates[template.Name()] = template.Lookup(template.Name())
}
return nil
}
// MustGet a template by name, panics when fails
func (t *Repository) MustGet(name string) *template.Template {
tpl, err := t.Get(name)
if err != nil {
panic(err)
}
return tpl
}
// AddFile adds a file to the repository. It will create a new template based on the filename.
// It trims the .gotmpl from the end and converts the name using swag.ToJSONName. This will strip
// directory separators and Camelcase the next letter.
// e.g validation/primitive.gotmpl will become validationPrimitive
//
// If the file contains a definition for a template that is protected the whole file will not be added
func (t *Repository) AddFile(name, data string) error {
return t.addFile(name, data, false)
}
// SetAllowOverride allows setting allowOverride after the Repository was initialized
func (t *Repository) SetAllowOverride(value bool) {
t.allowOverride = value
}
func findDependencies(n parse.Node) []string {
var deps []string
depMap := make(map[string]bool)
if n == nil {
return deps
}
switch node := n.(type) {
case *parse.ListNode:
if node != nil && node.Nodes != nil {
for _, nn := range node.Nodes {
for _, dep := range findDependencies(nn) {
depMap[dep] = true
}
}
}
case *parse.IfNode:
for _, dep := range findDependencies(node.BranchNode.List) {
depMap[dep] = true
}
for _, dep := range findDependencies(node.BranchNode.ElseList) {
depMap[dep] = true
}
case *parse.RangeNode:
for _, dep := range findDependencies(node.BranchNode.List) {
depMap[dep] = true
}
for _, dep := range findDependencies(node.BranchNode.ElseList) {
depMap[dep] = true
}
case *parse.WithNode:
for _, dep := range findDependencies(node.BranchNode.List) {
depMap[dep] = true
}
for _, dep := range findDependencies(node.BranchNode.ElseList) {
depMap[dep] = true
}
case *parse.TemplateNode:
depMap[node.Name] = true
}
for dep := range depMap {
deps = append(deps, dep)
}
return deps
}
func (t *Repository) flattenDependencies(templ *template.Template, dependencies map[string]bool) map[string]bool {
if dependencies == nil {
dependencies = make(map[string]bool)
}
deps := findDependencies(templ.Tree.Root)
for _, d := range deps {
if _, found := dependencies[d]; !found {
dependencies[d] = true
if tt := t.templates[d]; tt != nil {
dependencies = t.flattenDependencies(tt, dependencies)
}
}
dependencies[d] = true
}
return dependencies
}
func (t *Repository) addDependencies(templ *template.Template) (*template.Template, error) {
name := templ.Name()
deps := t.flattenDependencies(templ, nil)
for dep := range deps {
if dep == "" {
continue
}
tt := templ.Lookup(dep)
// Check if we have it
if tt == nil {
tt = t.templates[dep]
// Still don't have it, return an error
if tt == nil {
return templ, fmt.Errorf("could not find template %s", dep)
}
var err error
// Add it to the parse tree
templ, err = templ.AddParseTree(dep, tt.Tree)
if err != nil {
return templ, fmt.Errorf("dependency error: %v", err)
}
}
}
return templ.Lookup(name), nil
}
// Get will return the named template from the repository, ensuring that all dependent templates are loaded.
// It will return an error if a dependent template is not defined in the repository.
func (t *Repository) Get(name string) (*template.Template, error) {
templ, found := t.templates[name]
if !found {
return templ, fmt.Errorf("template doesn't exist %s", name)
}
return t.addDependencies(templ)
}
// DumpTemplates prints out a dump of all the defined templates, where they are defined and what their dependencies are.
func (t *Repository) DumpTemplates() {
buf := bytes.NewBuffer(nil)
fmt.Fprintln(buf, "\n# Templates")
for name, templ := range t.templates {
fmt.Fprintf(buf, "## %s\n", name)
fmt.Fprintf(buf, "Defined in `%s`\n", t.files[name])
if deps := findDependencies(templ.Tree.Root); len(deps) > 0 {
fmt.Fprintf(buf, "####requires \n - %v\n\n\n", strings.Join(deps, "\n - "))
}
fmt.Fprintln(buf, "\n---")
}
log.Println(buf.String())
}
// FuncMap functions
func asJSON(data interface{}) (string, error) {
b, err := json.Marshal(data)
if err != nil {
return "", err
}
return string(b), nil
}
func asPrettyJSON(data interface{}) (string, error) {
b, err := json.MarshalIndent(data, "", " ")
if err != nil {
return "", err
}
return string(b), nil
}
func pluralizeFirstWord(arg string) string {
sentence := strings.Split(arg, " ")
if len(sentence) == 1 {
return inflect.Pluralize(arg)
}
return inflect.Pluralize(sentence[0]) + " " + strings.Join(sentence[1:], " ")
}
func dropPackage(str string) string {
parts := strings.Split(str, ".")
return parts[len(parts)-1]
}
// return true if the GoType str contains pkg. For example "model.MyType" -> true, "MyType" -> false
func containsPkgStr(str string) bool {
dropped := dropPackage(str)
return !(dropped == str)
}
func padSurround(entry, padWith string, i, ln int) string {
var res []string
if i > 0 {
for j := 0; j < i; j++ {
res = append(res, padWith)
}
}
res = append(res, entry)
tot := ln - i - 1
for j := 0; j < tot; j++ {
res = append(res, padWith)
}
return strings.Join(res, ",")
}
func padComment(str string, pads ...string) string {
// pads specifes padding to indent multi line comments.Defaults to one space
pad := " "
lines := strings.Split(str, "\n")
if len(pads) > 0 {
pad = strings.Join(pads, "")
}
return (strings.Join(lines, "\n//"+pad))
}
func blockComment(str string) string {
return strings.ReplaceAll(str, "*/", "[*]/")
}
func pascalize(arg string) string {
runes := []rune(arg)
switch len(runes) {
case 0:
return "Empty"
case 1: // handle special case when we have a single rune that is not handled by swag.ToGoName
switch runes[0] {
case '+', '-', '#', '_', '*', '/', '=': // those cases are handled differently than swag utility
return prefixForName(arg)
}
}
return swag.ToGoName(swag.ToGoName(arg)) // want to remove spaces
}
func prefixForName(arg string) string {
first := []rune(arg)[0]
if len(arg) == 0 || unicode.IsLetter(first) {
return ""
}
switch first {
case '+':
return "Plus"
case '-':
return "Minus"
case '#':
return "HashTag"
case '*':
return "Asterisk"
case '/':
return "ForwardSlash"
case '=':
return "EqualSign"
// other cases ($,@ etc..) handled by swag.ToGoName
}
return "Nr"
}
func replaceSpecialChar(in rune) string {
switch in {
case '.':
return "-Dot-"
case '+':
return "-Plus-"
case '-':
return "-Dash-"
case '#':
return "-Hashtag-"
}
return string(in)
}
func cleanupEnumVariant(in string) string {
replaced := ""
for _, char := range in {
replaced += replaceSpecialChar(char)
}
return replaced
}
func dict(values ...interface{}) (map[string]interface{}, error) {
if len(values)%2 != 0 {
return nil, fmt.Errorf("expected even number of arguments, got %d", len(values))
}
dict := make(map[string]interface{}, len(values)/2)
for i := 0; i < len(values); i += 2 {
key, ok := values[i].(string)
if !ok {
return nil, fmt.Errorf("expected string key, got %+v", values[i])
}
dict[key] = values[i+1]
}
return dict, nil
}
func isInteger(arg interface{}) bool {
// is integer determines if a value may be represented by an integer
switch val := arg.(type) {
case int8, int16, int32, int, int64, uint8, uint16, uint32, uint, uint64:
return true
case *int8, *int16, *int32, *int, *int64, *uint8, *uint16, *uint32, *uint, *uint64:
v := reflect.ValueOf(arg)
return !v.IsNil()
case float64:
return math.Round(val) == val
case *float64:
return val != nil && math.Round(*val) == *val
case float32:
return math.Round(float64(val)) == float64(val)
case *float32:
return val != nil && math.Round(float64(*val)) == float64(*val)
case string:
_, err := strconv.ParseInt(val, 10, 64)
return err == nil
case *string:
if val == nil {
return false
}
_, err := strconv.ParseInt(*val, 10, 64)
return err == nil
default:
return false
}
}
func resolvedDocCollectionFormat(cf string, child *GenItems) string {
if child == nil {
return cf
}
ccf := cf
if ccf == "" {
ccf = "csv"
}
rcf := resolvedDocCollectionFormat(child.CollectionFormat, child.Child)
if rcf == "" {
return ccf
}
return ccf + "|" + rcf
}
func resolvedDocType(tn, ft string, child *GenItems) string {
if tn == "array" {
if child == nil {
return "[]any"
}
return "[]" + resolvedDocType(child.SwaggerType, child.SwaggerFormat, child.Child)
}
if ft != "" {
if doc, ok := docFormat[ft]; ok {
return doc
}
return fmt.Sprintf("%s (formatted %s)", ft, tn)
}
return tn
}
func resolvedDocSchemaType(tn, ft string, child *GenSchema) string {
if tn == "array" {
if child == nil {
return "[]any"
}
return "[]" + resolvedDocSchemaType(child.SwaggerType, child.SwaggerFormat, child.Items)
}
if tn == "object" {
if child == nil || child.ElemType == nil {
return "map of any"
}
if child.IsMap {
return "map of " + resolvedDocElemType(child.SwaggerType, child.SwaggerFormat, &child.resolvedType)
}
return child.GoType
}
if ft != "" {
if doc, ok := docFormat[ft]; ok {
return doc
}
return fmt.Sprintf("%s (formatted %s)", ft, tn)
}
return tn
}
func resolvedDocElemType(tn, ft string, schema *resolvedType) string {
if schema == nil {
return ""
}
if schema.IsMap {
return "map of " + resolvedDocElemType(schema.ElemType.SwaggerType, schema.ElemType.SwaggerFormat, schema.ElemType)
}
if schema.IsArray {
return "[]" + resolvedDocElemType(schema.ElemType.SwaggerType, schema.ElemType.SwaggerFormat, schema.ElemType)
}
if ft != "" {
if doc, ok := docFormat[ft]; ok {
return doc
}
return fmt.Sprintf("%s (formatted %s)", ft, tn)
}
return tn
}
func httpStatus(code int) string {
if name, ok := runtime.Statuses[code]; ok {
return name
}
// non-standard codes deserve some name
return fmt.Sprintf("Status %d", code)
}
func gt0(in *int64) bool {
// gt0 returns true if the *int64 points to a value > 0
// NOTE: plain {{ gt .MinProperties 0 }} just refuses to work normally
// with a pointer
return in != nil && *in > 0
}

View file

@ -0,0 +1,242 @@
// Code generated by go-swagger; DO NOT EDIT.
{{ if .Copyright -}}// {{ comment .Copyright -}}{{ end }}
package {{ .GenOpts.CliPackage }}
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
{{ imports .DefaultImports }}
{{ imports .Imports }}
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/go-openapi/runtime"
"github.com/go-openapi/swag"
httptransport "github.com/go-openapi/runtime/client"
homedir "github.com/mitchellh/go-homedir"
)
// debug flag indicating that cli should output debug logs
var debug bool
// config file location
var configFile string
// dry run flag
var dryRun bool
// name of the executable
var exeName string = filepath.Base(os.Args[0])
// logDebugf writes debug log to stdout
func logDebugf(format string, v ...interface{}) {
if !debug{
return
}
log.Printf(format, v...)
}
{{/*TODO: make this a swagger cli option*/}}
// depth of recursion to construct model flags
var maxDepth int = 5
// makeClient constructs a client object
func makeClient(cmd *cobra.Command, args []string) (*client.{{ pascalize .Name }}, error) {
hostname := viper.GetString("hostname")
viper.SetDefault("base_path", client.DefaultBasePath)
basePath := viper.GetString("base_path")
scheme := viper.GetString("scheme")
r := httptransport.New(hostname, basePath, []string{scheme})
r.SetDebug(debug)
{{- /* user might define custom mediatype xxx/json and there is no registered ones to handle. */}}
// set custom producer and consumer to use the default ones
{{ range .Consumes }}
{{ range .AllSerializers }}
{{- if stringContains .MediaType "json" }}
r.Consumers["{{ .MediaType }}"] = runtime.JSONConsumer()
{{- else }}
// warning: consumes {{ .MediaType }} is not supported by go-swagger cli yet
{{- end }}
{{- end }}
{{ end }}
{{ range .Produces }}
{{- range .AllSerializers }}
{{- if stringContains .MediaType "json" }}
r.Producers["{{ .MediaType }}"] = runtime.JSONProducer()
{{- else }}
// warning: produces {{ .MediaType }} is not supported by go-swagger cli yet
{{- end }}
{{- end }}
{{ end }}
{{- if .SecurityDefinitions }}
auth, err := makeAuthInfoWriter(cmd)
if err != nil {
return nil, err
}
r.DefaultAuthentication = auth
{{ end }}
appCli := client.New(r, strfmt.Default)
logDebugf("Server url: %v://%v", scheme, hostname)
return appCli, nil
}
// MakeRootCmd returns the root cmd
func MakeRootCmd() (*cobra.Command, error) {
cobra.OnInitialize(initViperConfigs)
// Use executable name as the command name
rootCmd := &cobra.Command{
Use: exeName,
}
{{/*note: viper binded flag value must be retrieved from viper rather than cmd*/}}
// register basic flags
rootCmd.PersistentFlags().String("hostname", client.DefaultHost, "hostname of the service")
viper.BindPFlag("hostname", rootCmd.PersistentFlags().Lookup("hostname"))
rootCmd.PersistentFlags().String("scheme", client.DefaultSchemes[0], fmt.Sprintf("Choose from: %v", client.DefaultSchemes))
viper.BindPFlag("scheme", rootCmd.PersistentFlags().Lookup("scheme"))
rootCmd.PersistentFlags().String("base-path", client.DefaultBasePath, fmt.Sprintf("For example: %v", client.DefaultBasePath))
viper.BindPFlag("base_path", rootCmd.PersistentFlags().Lookup("base-path"))
// configure debug flag
rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "output debug logs")
// configure config location
rootCmd.PersistentFlags().StringVar(&configFile, "config", "", "config file path")
// configure dry run flag
rootCmd.PersistentFlags().BoolVar(&dryRun, "dry-run", false, "do not send the request to server")
// register security flags
{{- if .SecurityDefinitions }}
if err := registerAuthInoWriterFlags(rootCmd); err != nil{
return nil, err
}
{{- end }}
// add all operation groups
{{- range .OperationGroups -}}
{{- $operationGroupCmdVarName := printf "operationGroup%vCmd" (pascalize .Name) }}
{{ $operationGroupCmdVarName }}, err := makeOperationGroup{{ pascalize .Name }}Cmd()
if err != nil {
return nil, err
}
rootCmd.AddCommand({{ $operationGroupCmdVarName }})
{{ end }}
// add cobra completion
rootCmd.AddCommand(makeGenCompletionCmd())
return rootCmd, nil
}
// initViperConfigs initialize viper config using config file in '$HOME/.config/<cli name>/config.<json|yaml...>'
// currently hostname, scheme and auth tokens can be specified in this config file.
func initViperConfigs() {
if configFile != "" {
// use user specified config file location
viper.SetConfigFile(configFile)
}else{
// look for default config
// Find home directory.
home, err := homedir.Dir()
cobra.CheckErr(err)
// Search config in home directory with name ".cobra" (without extension).
viper.AddConfigPath(path.Join(home, ".config", exeName))
viper.SetConfigName("config")
}
if err := viper.ReadInConfig(); err != nil {
logDebugf("Error: loading config file: %v", err)
return
}
logDebugf("Using config file: %v", viper.ConfigFileUsed())
}
{{- if .SecurityDefinitions }}
{{- /*youyuan: rework this since spec may define multiple auth schemes.
cli needs to detect which one user passed rather than add all of them.*/}}
// registerAuthInoWriterFlags registers all flags needed to perform authentication
func registerAuthInoWriterFlags(cmd *cobra.Command) error {
{{- range .SecurityDefinitions }}
/*{{.Name}} {{.Description}}*/
{{- if .IsBasicAuth }}
cmd.PersistentFlags().String("username", "", "username for basic auth")
viper.BindPFlag("username", cmd.PersistentFlags().Lookup("username"))
cmd.PersistentFlags().String("password", "", "password for basic auth")
viper.BindPFlag("password", cmd.PersistentFlags().Lookup("password"))
{{- end }}
{{- if .IsAPIKeyAuth }}
cmd.PersistentFlags().String("{{.Name}}", "", `{{.Description}}`)
viper.BindPFlag("{{.Name}}", cmd.PersistentFlags().Lookup("{{.Name}}"))
{{- end }}
{{- if .IsOAuth2 }}
// oauth2: let user provide the token in a flag, rather than implement the logic to fetch the token.
cmd.PersistentFlags().String("oauth2-token", "", `{{.Description}}`)
viper.BindPFlag("oauth2-token", cmd.PersistentFlags().Lookup("oauth2-token"))
{{- end }}
{{- end }}
return nil
}
// makeAuthInfoWriter retrieves cmd flags and construct an auth info writer
func makeAuthInfoWriter(cmd *cobra.Command) (runtime.ClientAuthInfoWriter, error) {
auths := []runtime.ClientAuthInfoWriter{}
{{- range .SecurityDefinitions }}
/*{{.Name}} {{.Description}}*/
{{- if .IsBasicAuth }}
if viper.IsSet("username") {
usr := viper.GetString("username")
if !viper.IsSet("password"){
return nil, fmt.Errorf("Basic Auth password for user [%v] is not provided.", usr)
}
pwd := viper.GetString("password")
auths = append(auths, httptransport.BasicAuth(usr,pwd))
}
{{- end }}
{{- if .IsAPIKeyAuth }}
if viper.IsSet("{{.Name}}") {
{{ pascalize .Name }}Key := viper.GetString("{{.Name}}")
auths = append(auths, httptransport.APIKeyAuth("{{.Name}}", "{{.In}}", {{ pascalize .Name }}Key))
}
{{- end }}
{{- if .IsOAuth2 }}
if viper.IsSet("oauth2-token") {
// oauth2 workflow for generated CLI is not ideal.
// If you have suggestions on how to support it, raise an issue here: https://github.com/go-swagger/go-swagger/issues
// This will be added to header: "Authorization: Bearer {oauth2-token value}"
token := viper.GetString("oauth2-token")
auths = append(auths, httptransport.BearerToken(token))
}
{{- end }}
{{- end }}
if len(auths) == 0 {
logDebugf("Warning: No auth params detected.")
return nil, nil
}
// compose all auths together
return httptransport.Compose(auths...), nil
}
{{- end }}
{{ range .OperationGroups -}}
func makeOperationGroup{{ pascalize .Name }}Cmd() (*cobra.Command, error) {
{{- $operationGroupCmdVarName := printf "operationGroup%vCmd" (pascalize .Name) }}
{{ $operationGroupCmdVarName }} := &cobra.Command{
Use: "{{ .Name }}",
Long: `{{ .Description }}`,
}
{{ range .Operations }}
{{- $operationCmdVarName := printf "operation%vCmd" (pascalize .Name) }}
{{ $operationCmdVarName }}, err := makeOperation{{pascalize .Package}}{{ pascalize .Name }}Cmd()
if err != nil {
return nil, err
}
{{ $operationGroupCmdVarName }}.AddCommand({{ $operationCmdVarName }})
{{ end }}
return {{ $operationGroupCmdVarName }}, nil
}
{{ end }} {{/*operation group*/}}

View file

@ -0,0 +1,77 @@
// Code generated by go-swagger; DO NOT EDIT.
{{ if .Copyright -}}// {{ comment .Copyright -}}{{ end }}
package {{ .GenOpts.CliPackage }}
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import(
"github.com/spf13/cobra"
)
func makeGenCompletionCmd() *cobra.Command{
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Long: `To load completions:
Bash:
$ source <(yourprogram completion bash)
# To load completions for each session, execute once:
# Linux:
$ yourprogram completion bash > /etc/bash_completion.d/yourprogram
# macOS:
$ yourprogram completion bash > /usr/local/etc/bash_completion.d/yourprogram
Zsh:
# If shell completion is not already enabled in your environment,
# you will need to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load completions for each session, execute once:
$ yourprogram completion zsh > "${fpath[1]}/_yourprogram"
# You will need to start a new shell for this setup to take effect.
fish:
$ yourprogram completion fish | source
# To load completions for each session, execute once:
$ yourprogram completion fish > ~/.config/fish/completions/yourprogram.fish
PowerShell:
PS> yourprogram completion powershell | Out-String | Invoke-Expression
# To load completions for every new session, run:
PS> yourprogram completion powershell > yourprogram.ps1
# and source this file from your PowerShell profile.
`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.ExactValidArgs(1),
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":
cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
}
},
}
return completionCmd
}

View file

@ -0,0 +1,28 @@
// Code generated by go-swagger; DO NOT EDIT.
{{ if .Copyright -}}// {{ comment .Copyright -}}{{ end }}
package main
import (
"encoding/json"
{{ imports .DefaultImports }}
{{ imports .Imports }}
)
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
func main() {
rootCmd,err := cli.MakeRootCmd()
if err != nil {
fmt.Println("Cmd construction error: ", err)
os.Exit(1)
}
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}

View file

@ -0,0 +1,25 @@
// Code generated by go-swagger; DO NOT EDIT.
{{ if .Copyright -}}// {{ comment .Copyright -}}{{ end }}
package cli
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
{{ imports .DefaultImports }}
{{ imports .Imports }}
"github.com/spf13/cobra"
)
// Schema cli for {{.GoType}}
{{ template "modelschemacli" .}}
{{ range .ExtraSchemas }}
// Extra schema cli for {{.GoType}}
{{ template "modelschemacli" .}}
{{ end }}

View file

@ -0,0 +1,230 @@
// Code generated by go-swagger; DO NOT EDIT.
{{ if .Copyright -}}// {{ comment .Copyright -}}{{ end }}
{{- /*TODO: do not hardcode cli pkg*/}}
package cli
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
{{ imports .DefaultImports }}
{{ imports .Imports }}
"github.com/spf13/cobra"
"github.com/go-openapi/runtime"
"github.com/go-openapi/swag"
httptransport "github.com/go-openapi/runtime/client"
)
// makeOperation{{pascalize .Package}}{{ pascalize .Name }}Cmd returns a cmd to handle operation {{ camelize .Name }}
func makeOperation{{pascalize .Package}}{{ pascalize .Name }}Cmd() (*cobra.Command, error) {
cmd := &cobra.Command{
Use: "{{ .Name }}",
Short: `{{ escapeBackticks .Description}}`,
RunE: runOperation{{pascalize .Package}}{{ pascalize .Name }},
}
if err := registerOperation{{pascalize .Package}}{{ pascalize .Name }}ParamFlags(cmd); err != nil{
return nil, err
}
return cmd, nil
}
{{ $operationGroup := .Package }}
{{ $operation := .Name }}
{{ $operationPkgAlias := .PackageAlias }}
// runOperation{{pascalize $operationGroup }}{{ pascalize $operation }} uses cmd flags to call endpoint api
func runOperation{{pascalize $operationGroup }}{{ pascalize $operation }}(cmd *cobra.Command, args []string) error{
appCli, err := makeClient(cmd, args)
if err != nil {
return err
}
// retrieve flag values from cmd and fill params
params := {{ .PackageAlias }}.New{{ pascalize .Name}}Params()
{{- range .Params }}
if err, _ := retrieveOperation{{pascalize $operationGroup }}{{ pascalize $operation }}{{ pascalize .Name }}Flag(params, "", cmd); err != nil{
return err
}
{{- end }} {{/*Params*/}}
if dryRun {
{{/* Note: dry run is not very useful for now, but useful when validation is added in future*/}}
logDebugf("dry-run flag specified. Skip sending request.")
return nil
}
// make request and then print result
{{- /*Package string is the operation name*/}}
msgStr, err := parseOperation{{pascalize .Package}}{{ pascalize .Name }}Result(appCli.{{- pascalize .Package }}.{{ pascalize .Name }}(params {{- if .Authorized}}, nil{{ end }}{{ if .HasStreamingResponse }}, &bytes.Buffer{}{{ end }}))
if err != nil {
return err
}
if !debug{
{{/* In debug mode content should have been printed in transport layer, so do not print again*/}}
fmt.Println(msgStr)
}
return nil
}
// registerOperation{{pascalize $operationGroup }}{{ pascalize $operation }}ParamFlags registers all flags needed to fill params
func registerOperation{{pascalize $operationGroup }}{{ pascalize $operation }}ParamFlags(cmd *cobra.Command) error {
{{- range .Params }}
if err := registerOperation{{pascalize $operationGroup }}{{ pascalize $operation }}{{pascalize .Name }}ParamFlags("", cmd); err != nil{
return err
}
{{- end }}
return nil
}
{{/*register functions for each fields in this operation*/}}
{{- range .Params }}
func registerOperation{{pascalize $operationGroup }}{{ pascalize $operation }}{{pascalize .Name }}ParamFlags(cmdPrefix string, cmd *cobra.Command) error{
{{- if .IsPrimitive }}
{{ template "primitiveregistrator" . }}
{{- else if .IsArray }}
{{ template "arrayregistrator" . }}
{{- else if and .IsBodyParam .Schema (not .IsArray) (not .IsMap) (not .IsStream) }}
{{ template "modelparamstringregistrator" . }}
{{ template "modelparamregistrator" . }}
{{/* Do not mark body flag as required, since the individial flag for body field will be added separately */}}
{{- else }}
// warning: go type {{ .GoType }} is not supported by go-swagger cli yet.
{{- end }}
return nil
}
{{- end }}
{{/*functions to retreive each field of params*/}}
{{- range .Params }}
func retrieveOperation{{pascalize $operationGroup }}{{ pascalize $operation }}{{ pascalize .Name }}Flag(m *{{ $operationPkgAlias }}.{{ pascalize $operation }}Params, cmdPrefix string, cmd *cobra.Command) (error,bool){
retAdded := false
{{- $flagStr := .Name }}
{{- $flagValueVar := printf "%vValue" (camelize .Name) }}
{{- /*only set the param if user set the flag*/}}
if cmd.Flags().Changed("{{ $flagStr }}") {
{{- if .IsPrimitive }}
{{ template "primitiveretriever" . }}
{{- else if .IsArray }}
{{ template "arrayretriever" . }}
{{- else if .IsMap }}
// warning: {{ .Name }} map type {{.GoType}} is not supported by go-swagger cli yet
{{- else if and .IsBodyParam .Schema .IsComplexObject (not .IsStream) }}
{{- /*schema payload can be passed in cmd as a string and here is unmarshalled to model struct and attached in params*/}}
// Read {{ $flagStr }} string from cmd and unmarshal
{{ $flagValueVar }}Str, err := cmd.Flags().GetString("{{ $flagStr }}")
if err != nil {
return err, false
}
{{/*Note anonymous body schema is not pointer*/}}
{{ $flagValueVar }} := {{if containsPkgStr .GoType}}{{ .GoType }}{{else}}{{ .Pkg }}.{{.GoType}}{{ end }}{}
if err := json.Unmarshal([]byte({{ $flagValueVar }}Str), &{{ $flagValueVar }}); err!= nil{
return fmt.Errorf("cannot unmarshal {{ $flagStr }} string in {{.GoType}}: %v", err), false
}
m.{{ .ID }} = {{- if .IsNullable }}&{{- end }}{{ $flagValueVar }}
{{- else }}
// warning: {{.GoType}} is not supported by go-swagger cli yet
{{- end }} {{/*end go type case*/}}
}
{{- if and .IsBodyParam .Schema .IsComplexObject (not .IsArray) (not .IsMap) (not .IsStream) }}
{{- /* Add flags to capture fields in Body. If previously Body struct was constructed in unmarshalling body string,
then reuse the struct, otherwise construct an empty value struct to fill. Here body field flags overwrites
unmarshalled body string values. */}}
{{- $flagModelVar := printf "%vModel" (camelize $flagValueVar) }}
{{ $flagModelVar }} := m.{{ .ID }}
if swag.IsZero({{ $flagModelVar }}){
{{ $flagModelVar }} = {{- if .IsNullable }}&{{- end }}{{if containsPkgStr .GoType}}{{ .GoType }}{{else}}{{ .Pkg }}.{{.GoType}}{{ end }}{}
}
{{- /*Only attach the body struct in params if user passed some flag filling some body fields.*/}}
{{- /* add "&" to $flagModelVar when it is not nullable because the retrieve method always expects a pointer */}}
err, added := retrieveModel{{ pascalize (dropPackage .GoType) }}Flags(0, {{if not .IsNullable}}&{{end}}{{ $flagModelVar }}, "{{ camelize (dropPackage .GoType) }}", cmd)
if err != nil{
return err, false
}
if added {
m.{{.ID}} = {{ $flagModelVar }}
}
if dryRun && debug {
{{/* dry run we don't get trasnport debug strings, so print it here*/}}
{{- $bodyDebugVar := printf "%vDebugBytes" (camelize $flagValueVar) }}
{{ $bodyDebugVar }}, err := json.Marshal(m.{{.ID}})
if err != nil{
return err, false
}
logDebugf("{{.ID }} dry-run payload: %v", string({{ $bodyDebugVar }}))
}
retAdded = retAdded || added
{{/*body debug string will be printed in transport layer*/}}
{{- end }}
return nil, retAdded
}
{{- end }} {{/*Params*/}}
// parseOperation{{pascalize .Package}}{{ pascalize .Name }}Result parses request result and return the string content
{{- /*TODO: handle multiple success response case*/}}
func parseOperation{{pascalize .Package}}{{ pascalize .Name }}Result({{- if .SuccessResponse }}{{ range $i, $v := .SuccessResponses }} resp{{$i}} *{{$v.Package}}.{{pascalize $v.Name}},{{- end }}{{- end }} respErr error) (string, error){
if respErr != nil {
{{- /*error is of type default model. If we can cast, then print the resp.*/}}
{{ if .DefaultResponse }} {{with .DefaultResponse}}
{{ if .Schema }}
var iRespD interface{} = respErr
respD, ok := iRespD.(*{{ .Package }}.{{ pascalize .Name }})
if ok {
if !swag.IsZero(respD) && !swag.IsZero(respD.Payload) {
msgStr,err := json.Marshal(respD.Payload)
if err != nil{
return "", err
}
return string(msgStr), nil
}
}
{{ else }}
// Non schema case: warning {{.Name}} is not supported
{{ end }}
{{ end }} {{ end }}
{{- range $i, $v := .Responses }}
{{ if .Schema }}
var iResp{{$i}} interface{} = respErr
resp{{$i}}, ok := iResp{{$i}}.(*{{ .Package }}.{{ pascalize .Name }})
if ok {
if !swag.IsZero(resp{{$i}}) && !swag.IsZero(resp{{$i}}.Payload) {
msgStr,err := json.Marshal(resp{{$i}}.Payload)
if err != nil{
return "", err
}
return string(msgStr), nil
}
}
{{ else }}
// Non schema case: warning {{.Name}} is not supported
{{ end }}
{{ end }}
return "", respErr
}
{{- range $i, $v := .SuccessResponses }}
{{ if .Schema }}
{{- with .Schema}}
if !swag.IsZero(resp{{$i}}) && !swag.IsZero(resp{{$i}}.Payload) {
{{- if or .IsComplexObject .IsArray .IsMap }}
msgStr,err := json.Marshal(resp{{$i}}.Payload)
if err != nil{
return "", err
}
{{- else }}
msgStr := fmt.Sprintf("%v", resp{{$i}}.Payload)
{{- end }}
return string(msgStr), nil
}
{{- end }}
{{ else }}
// warning: non schema response {{.Name}} is not supported by go-swagger cli yet.
{{ end }}
{{ end }}
return "", nil
}
{{/*for models defined in params, generate their register and retrieve flags functions*/}}
{{- range .ExtraSchemas }}
{{ template "modelschemacli" . }}
{{- end}}

View file

@ -0,0 +1,97 @@
{{/*util functions to run or register cmd flags*/}}
{{ define "flagdescriptionvar" }}
{{- $fullDescription := (escapeBackticks .Description) }}
{{- if .Required}}
{{- $fullDescription = printf "Required. %v" $fullDescription}}
{{- end}}
{{- if .Enum }}
{{- $fullDescription = printf "Enum: %v. %v" (json .Enum) $fullDescription}}
{{- end }}
{{ camelize .Name }}Description := `{{ $fullDescription }}`
{{ end }}
{{ define "flagnamevar" }}
{{- $flagNameVar := printf "%vFlagName" (camelize .Name) }}
var {{ $flagNameVar }} string
if cmdPrefix == "" {
{{ $flagNameVar }} = "{{ .Name }}"
}else{
{{ $flagNameVar }} = fmt.Sprintf("%v.{{ .Name }}", cmdPrefix)
}
{{ end }}
{{ define "flagdefaultvar" }}
{{ $defaultVar := printf "%vFlagDefault" (camelize .Name) }}
var {{ $defaultVar}} {{ .GoType }} {{ if .Default }}= {{ printf "%#v" .Default }}{{ end }}
{{ end }}
{{/* Not used. CLI does not mark flag as required, and required will be checked by validation in future */}}
{{/* {{ define "requiredregistrator" }}
if err := cmd.MarkPersistentFlagRequired({{ camelize .Name }}FlagName); err != nil{
return err
}
{{ end }} */}}
{{ define "enumcompletion" }} {{/*only used for primitive types. completion type is always string.*/}}
{{ if .Enum }}
if err := cmd.RegisterFlagCompletionFunc({{ camelize .Name }}FlagName,
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
var res []string
if err := json.Unmarshal([]byte(`{{ json .Enum }}`), &res); err != nil {
panic(err)
}
return res, cobra.ShellCompDirectiveDefault
}); err != nil{
return err
}
{{ end }}
{{ end }}
{{/* intended to be used on struct GenSchema with .IsPrimitive */}}
{{ define "primitiveregistrator" }}
{{- if or (eq .GoType "int64") (eq .GoType "int32") (eq .GoType "string") (eq .GoType "float64") (eq .GoType "float32") (eq .GoType "bool") }}
{{ template "flagdescriptionvar" . }}
{{ template "flagnamevar" . }}
{{ template "flagdefaultvar" . }}
_ = cmd.PersistentFlags().{{ pascalize .GoType }}({{ camelize .Name }}FlagName, {{ camelize .Name }}FlagDefault, {{ (camelize .Name) }}Description)
{{ template "enumcompletion" . }}
{{- else if or (eq .GoType "strfmt.DateTime") (eq .GoType "strfmt.UUID") (eq .GoType "strfmt.ObjectId") }} {{/* read as string */}}
{{ template "flagdescriptionvar" . }}
{{ template "flagnamevar" . }}
_ = cmd.PersistentFlags().String({{ camelize .Name }}FlagName, "", {{ (camelize .Name) }}Description)
{{ template "enumcompletion" . }}
{{- else }}
// warning: primitive {{.Name}} {{.GoType }} is not supported by go-swagger cli yet
{{- end }}
{{ end }}
{{ define "arrayregistrator" }}
{{- if or (eq .GoType "[]int64") (eq .GoType "[]int32") (eq .GoType "[]string") (eq .GoType "[]float64") (eq .GoType "[]float32") (eq .GoType "[]bool") }}
{{ template "flagdescriptionvar" . }}
{{ template "flagnamevar" . }}
{{ template "flagdefaultvar" . }}
_ = cmd.PersistentFlags().{{ pascalize .GoType }}Slice({{ camelize .Name }}FlagName, {{ camelize .Name }}FlagDefault, {{ (camelize .Name) }}Description)
{{ template "enumcompletion" . }}
{{- else if or (eq .GoType "[]strfmt.DateTime") (eq .GoType "[]strfmt.UUID") (eq .GoType "[]strfmt.ObjectId") }} {{/* read as string */}}
{{ template "flagdescriptionvar" . }}
{{ template "flagnamevar" . }}
_ = cmd.PersistentFlags().StringSlice({{ camelize .Name }}FlagName, []string{}, {{ (camelize .Name) }}Description)
{{- else }}
// warning: array {{.Name}} {{.GoType }} is not supported by go-swagger cli yet
{{- end }}
{{ end }}
{{/* each body parameter gets a string flag to input json raw string */}}
{{ define "modelparamstringregistrator" }}
{{ template "flagnamevar" . }}
_ = cmd.PersistentFlags().String({{ camelize .Name }}FlagName, "", "Optional json string for [{{ .Name }}]. {{ .Description }}")
{{ end }}
{{ define "modelparamregistrator" }} {{/* register a param that has a schema */}}
// add flags for body {{/*use go type as the flag prefix. There is no good way to determine the original str case in spec*/}}
if err := registerModel{{ pascalize (dropPackage .GoType) }}Flags(0, "{{ camelize (dropPackage .GoType) }}", cmd); err != nil {
return err
}
{{ end }}

View file

@ -0,0 +1,59 @@
{{/*util functions to retrieve flags*/}}
{{ define "primitiveretriever" }}
{{- $flagValueVar := printf "%vFlagValue" (camelize .Name) }}
{{- $flagNameVar := printf "%vFlagName" (camelize .Name )}}
{{- if or (eq .GoType "int64") (eq .GoType "int32") (eq .GoType "string") (eq .GoType "float64") (eq .GoType "float32") (eq .GoType "bool") }}
{{ template "flagnamevar" . }}
{{ $flagValueVar }}, err := cmd.Flags().Get{{pascalize .GoType}}({{ $flagNameVar }})
if err != nil{
return err, false
}
{{- /* reciever by convention is m for CLI */}}
m.{{ pascalize .Name }} = {{- if .IsNullable }}&{{- end }}{{ $flagValueVar }}
{{- else if or (eq .GoType "strfmt.DateTime") (eq .GoType "strfmt.ObjectId") (eq .GoType "strfmt.UUID" ) }} {{/*Get flag value as string, then parse it*/}}
{{/*Many of the strfmt types can be added here*/}}
{{ template "flagnamevar" . }}
{{ $flagValueVar }}Str, err := cmd.Flags().GetString({{ $flagNameVar }})
if err != nil{
return err, false
}
var {{ $flagValueVar }} {{ .GoType }}
if err := {{ $flagValueVar }}.UnmarshalText([]byte({{ $flagValueVar }}Str)); err != nil{
return err, false
}
m.{{ pascalize .Name }} = {{- if .IsNullable }}&{{- end }}{{ $flagValueVar }}
{{- else }}
// warning: primitive {{.Name}} {{.GoType }} is not supported by go-swagger cli yet
{{- end }}
{{ end }}
{{ define "arrayretriever" }}
{{- $flagValueVar := printf "%vFlagValues" (camelize .Name) }}
{{- $flagNameVar := printf "%vFlagName" (camelize .Name )}}
{{- if or (eq .GoType "[]int64") (eq .GoType "[]int32") (eq .GoType "[]string") (eq .GoType "[]float64") (eq .GoType "[]float32") (eq .GoType "[]bool") }}
{{ template "flagnamevar" . }}
{{ $flagValueVar }}, err := cmd.Flags().Get{{pascalize .GoType}}Slice({{ $flagNameVar }})
if err != nil{
return err, false
}
{{- /* reciever by convention is m for CLI */}}
m.{{ pascalize .Name }} = {{ $flagValueVar }}
{{- else if or (eq .GoType "[]strfmt.DateTime") (eq .GoType "[]strfmt.ObjectId") (eq .GoType "[]strfmt.UUID") }} {{/*Get flag value as string, then parse it*/}}
{{ template "flagnamevar" . }}
{{ $flagValueVar }}Str, err := cmd.Flags().GetStringSlice({{ $flagNameVar }})
if err != nil{
return err, false
}
{{ $flagValueVar }} := make({{ .GoType }}, len({{ $flagValueVar }}Str))
for i, v := range {{ $flagValueVar }}Str {
if err := {{ $flagValueVar }}[i].UnmarshalText([]byte(v)); err != nil{
return err, false
}
}
m.{{ pascalize .Name }} = {{- if .IsNullable }}&{{- end }}{{ $flagValueVar }}
{{- else }}
// warning: array {{.Name}} {{.GoType }} is not supported by go-swagger cli yet
{{- end }}
{{ end }}

View file

@ -0,0 +1,193 @@
{{/*util functions to generate register and retrieve functions for a model*/}}
{{ define "modelschemacli" }}
{{/*some guards to prevent rendering unsupported models types. TODO: remove this guard*/}}
{{if or .IsPrimitive .IsComplexObject }}
{{ template "modelschemacliinternal" . }}
{{ else }}
// Name: [{{.Name}}], Type:[{{ .GoType }}], register and retrieve functions are not rendered by go-swagger cli
{{ end }}
{{ end }}
{{/*since register and retrieve are the same for properties and all of, share them here*/}}
{{ define "propertyregistor" }}
{{- if .IsPrimitive }}
{{ template "primitiveregistrator" . }}
{{- else if .IsArray }}
// warning: {{.Name}} {{ .GoType }} array type is not supported by go-swagger cli yet
{{- else if .IsMap }}
// warning: {{.Name}} {{ .GoType }} map type is not supported by go-swagger cli yet
{{- else if .IsComplexObject }} {{/* struct case */}}
{{ template "flagnamevar" . }}
if err := registerModel{{pascalize (dropPackage .GoType) }}Flags(depth + 1, {{ camelize .Name }}FlagName, cmd); err != nil{
return err
}
{{- else }}
// warning: {{.Name}} {{ .GoType }} unkown type is not supported by go-swagger cli yet
{{- end }}
{{ end }}
{{ define "propertyretriever" }}
{{- $flagNameVar := printf "%vFlagName" (camelize .Name) }}
{{- $flagValueVar := printf "%vFlagValue" (camelize .Name) }}
{{ $flagNameVar }} := fmt.Sprintf("%v.{{ .Name }}", cmdPrefix)
if cmd.Flags().Changed({{ $flagNameVar }}) {
{{- if .IsPrimitive }}
{{ template "primitiveretriever" . }}
retAdded = true
{{- else if .IsArray }}
// warning: {{ .Name }} array type {{ .GoType }} is not supported by go-swagger cli yet
{{- else if .IsMap }}
// warning: {{ .Name }} map type {{ .GoType }} is not supported by go-swagger cli yet
{{- else if .IsComplexObject }}
// info: complex object {{.Name}} {{.GoType}} is retrieved outside this Changed() block
{{- else }}
// warning: {{.Name}} {{ .GoType }} unkown type is not supported by go-swagger cli yet
{{- end }}
}
{{- if and .IsComplexObject (not .IsArray) (not .IsMap) (not .IsStream) }}
{{ $flagValueVar }} := m.{{pascalize .Name}}
if swag.IsZero({{ $flagValueVar }}){
{{ $flagValueVar }} = {{if .IsNullable }}&{{end}}{{if containsPkgStr .GoType}}{{ .GoType }}{{else}}{{ .Pkg }}.{{.GoType}}{{ end }}{}
}
{{/* always lift the payload to pointer and pass to model retrieve function. If .GoType has pkg str, use it, else use .Pkg+.GoType */}}
err, {{camelize .Name }}Added := retrieveModel{{pascalize (dropPackage .GoType) }}Flags(depth + 1, {{if not .IsNullable }}&{{end}}{{ $flagValueVar }}, {{ $flagNameVar }}, cmd)
if err != nil{
return err, false
}
retAdded = retAdded || {{camelize .Name }}Added
if {{camelize .Name }}Added {
m.{{pascalize .Name}} = {{ $flagValueVar }}
}
{{- end }}
{{ end }}
{{ define "modelschemacliinternal" }} {{/*used by model definition and in params model*/}}
{{- $modelName := .Name }}
{{/*model package is filled by generator*/}}
{{ $modelPkg := toPackageName .Pkg}}
{{ $modelType := .GoType }}
// register flags to command
func registerModel{{pascalize .Name}}Flags(depth int, cmdPrefix string, cmd *cobra.Command) error {
{{ range .AllOf }}
{{- if not .IsAnonymous }}{{/* named type composition */}}
{{ if or .IsPrimitive .IsComplexObject }}
// register embedded {{ .GoType }} flags
{{/*defer all of registration to the model's regristor method. embed should not lift cmdPrefix */}}
if err := registerModel{{ pascalize (dropPackage .GoType) }}Flags(depth, cmdPrefix, cmd); err != nil{
return err
}
{{ else }}
// {{ .Name }} {{ .GoType }} register is skipped
{{ end }}
{{ else }}{{/*inline definition. assume only properties are used*/}}
// register anonymous fields for {{.Name}}
{{ $anonName := .Name }}
{{ range .Properties }}
if err := register{{ pascalize $modelName }}Anon{{pascalize $anonName }}{{ pascalize .Name }}(depth, cmdPrefix, cmd); err != nil{
return err
}
{{ end }}
{{ end }}
{{ end }}
{{ range .Properties }}
if err := register{{ pascalize $modelName }}{{ pascalize .Name }}(depth, cmdPrefix, cmd); err != nil{
return err
}
{{ end }}
return nil
}
{{ range .AllOf }}
{{- if .IsAnonymous }}{{/* inline definition. schema case is defered. */}}
// inline definition name {{ .Name }}, type {{.GoType}}
{{ $anonName := .Name }}
{{ range .Properties }}
func register{{ pascalize $modelName }}Anon{{pascalize $anonName }}{{ pascalize .Name }}(depth int, cmdPrefix string, cmd *cobra.Command) error {
if depth > maxDepth {
return nil
}
{{ template "propertyregistor" . }}
return nil
}
{{ end }}
{{ end }}
{{ end }}
{{/*register functions for each fields in this model */}}
{{ range .Properties }}
func register{{ pascalize $modelName }}{{ pascalize .Name }}(depth int, cmdPrefix string, cmd *cobra.Command) error{
if depth > maxDepth {
return nil
}
{{ template "propertyregistor" .}}
return nil
}
{{ end }} {{/*Properties*/}}
// retrieve flags from commands, and set value in model. Return true if any flag is passed by user to fill model field.
func retrieveModel{{pascalize $modelName }}Flags(depth int, m *{{if containsPkgStr .GoType}}{{ .GoType }}{{else}}{{ .Pkg }}.{{.GoType}}{{ end }}, cmdPrefix string, cmd *cobra.Command) (error, bool) {
retAdded := false
{{ range .AllOf }}
{{- if not .IsAnonymous }}{{/* named type composition */}}
{{ if or .IsPrimitive .IsComplexObject }}
// retrieve model {{.GoType}}
err, {{camelize .Name }}Added := retrieveModel{{ pascalize (dropPackage .GoType) }}Flags(depth, &m.{{pascalize (dropPackage .GoType) }}, cmdPrefix, cmd)
if err != nil{
return err, false
}
retAdded = retAdded || {{camelize .Name }}Added
{{ else }} {{/*inline anonymous case*/}}
{{ end }}
{{- else }}
// retrieve allOf {{.Name}} fields
{{ $anonName := .Name }}
{{ range .Properties }}
err, {{camelize .Name}}Added := retrieve{{ pascalize $modelName }}Anon{{pascalize $anonName }}{{ pascalize .Name }}Flags(depth, m, cmdPrefix, cmd)
if err != nil{
return err, false
}
retAdded = retAdded || {{ camelize .Name }}Added
{{ end }}
{{- end }}
{{ end }}
{{ range .Properties }}
err, {{ camelize .Name }}Added := retrieve{{pascalize $modelName }}{{pascalize .Name }}Flags(depth, m, cmdPrefix, cmd)
if err != nil{
return err, false
}
retAdded = retAdded || {{ camelize .Name }}Added
{{ end }}
return nil, retAdded
}
{{ range .AllOf }}
{{- if .IsAnonymous }}{{/* inline definition. schema case is defered. */}}
// define retrieve functions for fields for inline definition name {{ .Name }}
{{ $anonName := .Name }}
{{ range .Properties }} {{/*anonymous fields will be registered directly on parent model*/}}
func retrieve{{ pascalize $modelName }}Anon{{pascalize $anonName }}{{ pascalize .Name }}Flags(depth int, m *{{if containsPkgStr $modelType}}{{ $modelType }}{{else}}{{ $modelPkg }}.{{$modelType}}{{ end }},cmdPrefix string, cmd *cobra.Command) (error,bool) {
if depth > maxDepth {
return nil, false
}
retAdded := false
{{ template "propertyretriever" . }}
return nil, retAdded
}
{{ end }}
{{ end }}
{{ end }}
{{ range .Properties }}
func retrieve{{pascalize $modelName }}{{pascalize .Name }}Flags(depth int, m *{{if $modelPkg}}{{$modelPkg}}.{{ dropPackage $modelType }}{{else}}{{ $modelType }}{{end}}, cmdPrefix string, cmd *cobra.Command) (error, bool) {
if depth > maxDepth {
return nil, false
}
retAdded := false
{{ template "propertyretriever" . }}
return nil, retAdded
}
{{ end }} {{/*properties*/}}
{{ end }} {{/*define*/}}

View file

@ -0,0 +1,127 @@
// Code generated by go-swagger; DO NOT EDIT.
{{ if .Copyright -}}// {{ comment .Copyright -}}{{ end }}
package {{ .Name }}
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"fmt"
"io"
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
{{ imports .DefaultImports }}
{{ imports .Imports }}
)
// New creates a new {{ humanize .Name }} API client.
func New(transport runtime.ClientTransport, formats strfmt.Registry) ClientService {
return &Client{transport: transport, formats: formats}
}
/*
Client {{ if .Summary }}{{ .Summary }}{{ if .Description }}
{{ blockcomment .Description }}{{ end }}{{ else if .Description}}{{ blockcomment .Description }}{{ else }}for {{ humanize .Name }} API{{ end }}
*/
type Client struct {
transport runtime.ClientTransport
formats strfmt.Registry
}
// ClientOption is the option for Client methods
type ClientOption func(*runtime.ClientOperation)
// ClientService is the interface for Client methods
type ClientService interface {
{{ range .Operations }}
{{ pascalize .Name }}(params *{{ pascalize .Name }}Params{{ if .Authorized }}, authInfo runtime.ClientAuthInfoWriter{{end}}{{ if .HasStreamingResponse }}, writer io.Writer{{ end }}, opts ...ClientOption) {{ if .SuccessResponse }}({{ range .SuccessResponses }}*{{ pascalize .Name }}, {{ end }}{{ end }}error{{ if .SuccessResponse }}){{ end }}
{{ end }}
SetTransport(transport runtime.ClientTransport)
}
{{ range .Operations }}
/*
{{ pascalize .Name }} {{ if .Summary }}{{ pluralizeFirstWord (humanize .Summary) }}{{ if .Description }}
{{ blockcomment .Description }}{{ end }}{{ else if .Description}}{{ blockcomment .Description }}{{ else }}{{ humanize .Name }} API{{ end }}
*/
func (a *Client) {{ pascalize .Name }}(params *{{ pascalize .Name }}Params{{ if .Authorized }}, authInfo runtime.ClientAuthInfoWriter{{end}}{{ if .HasStreamingResponse }}, writer io.Writer{{ end }}, opts ...ClientOption) {{ if .SuccessResponse }}({{ range .SuccessResponses }}*{{ pascalize .Name }}, {{ end }}{{ end }}error{{ if .SuccessResponse }}){{ end }} {
// TODO: Validate the params before sending
if params == nil {
params = New{{ pascalize .Name }}Params()
}
op := &runtime.ClientOperation{
ID: {{ printf "%q" .Name }},
Method: {{ printf "%q" .Method }},
PathPattern: {{ printf "%q" .Path }},
ProducesMediaTypes: {{ printf "%#v" .ProducesMediaTypes }},
ConsumesMediaTypes: {{ printf "%#v" .ConsumesMediaTypes }},
Schemes: {{ printf "%#v" .Schemes }},
Params: params,
Reader: &{{ pascalize .Name }}Reader{formats: a.formats{{ if .HasStreamingResponse }}, writer: writer{{ end }}},{{ if .Authorized }}
AuthInfo: authInfo,{{ end}}
Context: params.Context,
Client: params.HTTPClient,
}
for _, opt := range opts {
opt(op)
}
{{ $length := len .SuccessResponses }}
{{ if .SuccessResponse }}result{{else}}_{{ end }}, err := a.transport.Submit(op)
if err != nil {
return {{ if .SuccessResponse }}{{ padSurround "nil" "nil" 0 $length }}, {{ end }}err
}
{{- if .SuccessResponse }}
{{- if eq $length 1 }}
success, ok := result.(*{{ pascalize .SuccessResponse.Name }})
if ok {
return success,nil
}
// unexpected success response
{{- if .DefaultResponse }}{{/* if a default response is provided, fill this and return an error */}}
unexpectedSuccess := result.(*{{ pascalize .DefaultResponse.Name }})
return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code())
{{- else }}
// safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue
msg := fmt.Sprintf("unexpected success response for {{ .Name }}: API contract not enforced by server. Client expected to get an error, but got: %T", result)
panic(msg)
{{- end }}
{{- else }}{{/* several possible success responses */}}
switch value := result.(type) {
{{- range $i, $v := .SuccessResponses }}
case *{{ pascalize $v.Name }}:
return {{ padSurround "value" "nil" $i $length }}, nil
{{- end }}
}
{{- if .DefaultResponse }}{{/* if a default response is provided, fill this and return an error */}}
// unexpected success response
unexpectedSuccess := result.(*{{ pascalize .DefaultResponse.Name }})
return {{ padSurround "nil" "nil" 0 $length }}, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code())
{{- else }}
// safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue
msg := fmt.Sprintf("unexpected success response for {{ $.Name }}: API contract not enforced by server. Client expected to get an error, but got: %T", result)
panic(msg)
{{- end }}
{{- end }}
{{- else }}
return nil
{{- end }}
}
{{- end }}
// SetTransport changes the transport on the client
func (a *Client) SetTransport(transport runtime.ClientTransport) {
a.transport = transport
}

View file

@ -0,0 +1,129 @@
// Code generated by go-swagger; DO NOT EDIT.
{{ if .Copyright -}}// {{ comment .Copyright -}}{{ end }}
package {{ .Package }}
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
httptransport "github.com/go-openapi/runtime/client"
"github.com/go-openapi/spec"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
{{ imports .DefaultImports }}
{{ imports .Imports }}
)
// Default {{ humanize .Name }} HTTP client.
var Default = NewHTTPClient(nil)
const (
// DefaultHost is the default Host
// found in Meta (info) section of spec file
DefaultHost string = {{ printf "%#v" .Host }}
// DefaultBasePath is the default BasePath
// found in Meta (info) section of spec file
DefaultBasePath string = {{ printf "%#v" .BasePath }}
)
// DefaultSchemes are the default schemes found in Meta (info) section of spec file
var DefaultSchemes = {{ printf "%#v" .Schemes }}
// NewHTTPClient creates a new {{ humanize .Name }} HTTP client.
func NewHTTPClient(formats strfmt.Registry) *{{ pascalize .Name }} {
return NewHTTPClientWithConfig(formats, nil)
}
// NewHTTPClientWithConfig creates a new {{ humanize .Name }} HTTP client,
// using a customizable transport config.
func NewHTTPClientWithConfig(formats strfmt.Registry, cfg *TransportConfig) *{{ pascalize .Name }} {
// ensure nullable parameters have default
if cfg == nil {
cfg = DefaultTransportConfig()
}
// create transport and client
transport := httptransport.New(cfg.Host, cfg.BasePath, cfg.Schemes)
return New(transport, formats)
}
// New creates a new {{ humanize .Name }} client
func New(transport runtime.ClientTransport, formats strfmt.Registry) *{{ pascalize .Name }} {
// ensure nullable parameters have default
if formats == nil {
formats = strfmt.Default
}
cli := new({{ pascalize .Name }})
cli.Transport = transport
{{- range .OperationGroups }}
cli.{{ pascalize .Name }} = {{ .PackageAlias }}.New(transport, formats)
{{- end }}
return cli
}
// DefaultTransportConfig creates a TransportConfig with the
// default settings taken from the meta section of the spec file.
func DefaultTransportConfig() *TransportConfig {
return &TransportConfig {
Host: DefaultHost,
BasePath: DefaultBasePath,
Schemes: DefaultSchemes,
}
}
// TransportConfig contains the transport related info,
// found in the meta section of the spec file.
type TransportConfig struct {
Host string
BasePath string
Schemes []string
}
// WithHost overrides the default host,
// provided by the meta section of the spec file.
func (cfg *TransportConfig) WithHost(host string) *TransportConfig {
cfg.Host = host
return cfg
}
// WithBasePath overrides the default basePath,
// provided by the meta section of the spec file.
func (cfg *TransportConfig) WithBasePath(basePath string) *TransportConfig {
cfg.BasePath = basePath
return cfg
}
// WithSchemes overrides the default schemes,
// provided by the meta section of the spec file.
func (cfg *TransportConfig) WithSchemes(schemes []string) *TransportConfig {
cfg.Schemes = schemes
return cfg
}
// {{ pascalize .Name }} is a client for {{ humanize .Name }}
type {{ pascalize .Name }} struct {
{{ range .OperationGroups }}
{{ pascalize .Name }} {{ .PackageAlias }}.ClientService
{{ end }}
Transport runtime.ClientTransport
}
// SetTransport changes the transport on the client and all its subresources
func (c *{{pascalize .Name}}) SetTransport(transport runtime.ClientTransport) {
c.Transport = transport
{{- range .OperationGroups }}
c.{{ pascalize .Name }}.SetTransport(transport)
{{- end }}
}

View file

@ -0,0 +1,406 @@
// Code generated by go-swagger; DO NOT EDIT.
{{ if .Copyright -}}// {{ comment .Copyright -}}{{ end }}
package {{ .Package }}
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"fmt"
"net/http"
"time"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
cr "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
{{ imports .DefaultImports }}
{{ imports .Imports }}
)
// New{{ pascalize .Name }}Params creates a new {{ pascalize .Name }}Params object,
// with the default timeout for this client.
//
// Default values are not hydrated, since defaults are normally applied by the API server side.
//
// To enforce default values in parameter, use SetDefaults or WithDefaults.
func New{{ pascalize .Name }}Params() *{{ pascalize .Name }}Params {
return &{{ pascalize .Name}}Params{
{{ camelize .TimeoutName }}: cr.DefaultTimeout,
}
}
// New{{ pascalize .Name }}ParamsWithTimeout creates a new {{ pascalize .Name }}Params object
// with the ability to set a timeout on a request.
func New{{ pascalize .Name }}ParamsWithTimeout(timeout time.Duration) *{{ pascalize .Name }}Params {
return &{{ pascalize .Name}}Params{
{{ camelize .TimeoutName }}: timeout,
}
}
// New{{ pascalize .Name }}ParamsWithContext creates a new {{ pascalize .Name }}Params object
// with the ability to set a context for a request.
func New{{ pascalize .Name }}ParamsWithContext(ctx context.Context) *{{ pascalize .Name }}Params {
return &{{ pascalize .Name}}Params{
Context: ctx,
}
}
// New{{ pascalize .Name }}ParamsWithHTTPClient creates a new {{ pascalize .Name }}Params object
// with the ability to set a custom HTTPClient for a request.
func New{{ pascalize .Name }}ParamsWithHTTPClient(client *http.Client) *{{ pascalize .Name }}Params {
return &{{ pascalize .Name}}Params{
HTTPClient: client,
}
}
/* {{ pascalize .Name }}Params contains all the parameters to send to the API endpoint
for the {{ humanize .Name }} operation.
Typically these are written to a http.Request.
*/
type {{ pascalize .Name }}Params struct {
{{- range .Params }}
{{- if .Description }}
/* {{ pascalize .Name }}.
{{ blockcomment .Description }}
{{- if or .SwaggerFormat .Default }}
{{ print "" }}
{{- if .SwaggerFormat }}
Format: {{ .SwaggerFormat }}
{{- end }}
{{- if .Default }}
Default: {{ json .Default }}
{{- end }}
{{- end }}
*/
{{- else }}
// {{ pascalize .Name }}.
{{- if or .SwaggerFormat .Default }}
//
{{- if .SwaggerFormat }}
// Format: {{ .SwaggerFormat }}
{{- end }}
{{- if .Default }}
// Default: {{ json .Default }}
{{- end }}
{{- end }}
{{- end }}
{{ pascalize .ID }} {{ if and (not .IsArray) (not .IsMap) (not .HasDiscriminator) (not .IsInterface) (not .IsStream) (or .IsNullable ) }}*{{ end }}{{ if not .IsFileParam }}{{ .GoType }}{{ else }}runtime.NamedReadCloser{{ end }}
{{- end }}
{{ camelize .TimeoutName }} time.Duration
Context context.Context
HTTPClient *http.Client
}
// WithDefaults hydrates default values in the {{ humanize .Name }} params (not the query body).
//
// All values with no default are reset to their zero value.
func ({{ .ReceiverName }} *{{ pascalize .Name }}Params) WithDefaults() *{{ pascalize .Name }}Params {
{{ .ReceiverName }}.SetDefaults()
return {{ .ReceiverName }}
}
// SetDefaults hydrates default values in the {{ humanize .Name }} params (not the query body).
//
// All values with no default are reset to their zero value.
func ({{ .ReceiverName }} *{{ pascalize .Name }}Params) SetDefaults() {
{{- if .Params.HasSomeDefaults }}
var (
{{- range .Params }}
{{- if .HasDefault }}
{{- if .IsFileParam }}
// NOTE: no default supported for file parameter {{ .ID }}
{{- else if .IsStream }}
// NOTE: no default supported for stream parameter {{ .ID }}
{{- else if not .IsBodyParam }}
{{ template "simpleschemaDefaultsvar" . }}
{{- end }}
{{- end }}
{{- end }}
)
{{- range .Params }}
{{- if and .HasDefault (not .IsFileParam) (not .IsStream) (not .IsBodyParam) }}
{{ template "simpleschemaDefaultsinit" . }}
{{- end }}
{{- end }}
val := {{ pascalize .Name }}Params{
{{- range .Params }}
{{- if and .HasDefault (not .IsFileParam) (not .IsStream) (not .IsBodyParam) }}
{{ pascalize .ID }}: {{ if and (not .IsArray) (not .IsMap) (not .HasDiscriminator) (or .IsNullable ) }}&{{ end }}{{ varname .ID }}Default,
{{- end }}
{{- end }}
}
val.{{ camelize .TimeoutName }} = {{ .ReceiverName }}.{{ camelize .TimeoutName }}
val.Context = {{ .ReceiverName }}.Context
val.HTTPClient = {{ .ReceiverName }}.HTTPClient
*{{ .ReceiverName }} = val
{{- else }}
// no default values defined for this parameter
{{- end }}
}
// With{{ pascalize .TimeoutName }} adds the timeout to the {{ humanize .Name }} params
func ({{ .ReceiverName }} *{{ pascalize .Name }}Params) With{{ pascalize .TimeoutName }}(timeout time.Duration) *{{ pascalize .Name }}Params {
{{ .ReceiverName }}.Set{{ pascalize .TimeoutName }}(timeout)
return {{ .ReceiverName }}
}
// Set{{ pascalize .TimeoutName }} adds the timeout to the {{ humanize .Name }} params
func ({{ .ReceiverName }} *{{ pascalize .Name }}Params) Set{{ pascalize .TimeoutName }}(timeout time.Duration) {
{{ .ReceiverName }}.{{ camelize .TimeoutName }} = timeout
}
// WithContext adds the context to the {{ humanize .Name }} params
func ({{ .ReceiverName }} *{{ pascalize .Name }}Params) WithContext(ctx context.Context) *{{ pascalize .Name }}Params {
{{ .ReceiverName }}.SetContext(ctx)
return {{ .ReceiverName }}
}
// SetContext adds the context to the {{ humanize .Name }} params
func ({{ .ReceiverName }} *{{ pascalize .Name }}Params) SetContext(ctx context.Context) {
{{ .ReceiverName }}.Context = ctx
}
// WithHTTPClient adds the HTTPClient to the {{ humanize .Name }} params
func ({{ .ReceiverName }} *{{ pascalize .Name }}Params) WithHTTPClient(client *http.Client) *{{ pascalize .Name }}Params {
{{ .ReceiverName }}.SetHTTPClient(client)
return {{ .ReceiverName }}
}
// SetHTTPClient adds the HTTPClient to the {{ humanize .Name }} params
func ({{ .ReceiverName }} *{{ pascalize .Name }}Params) SetHTTPClient(client *http.Client) {
{{ .ReceiverName }}.HTTPClient = client
}
{{- range .Params }}
// With{{ pascalize .ID }} adds the {{ varname .Name }} to the {{ humanize $.Name }} params
func ({{ $.ReceiverName }} *{{ pascalize $.Name }}Params) With{{ pascalize .ID }}({{ varname .Name }} {{ if and (not .IsArray) (not .IsMap) (not .HasDiscriminator) (not .IsStream) (or .IsNullable ) }}*{{ end }}{{ if not .IsFileParam }}{{ .GoType }}{{ else }}runtime.NamedReadCloser{{ end }}) *{{ pascalize $.Name }}Params {
{{ $.ReceiverName }}.Set{{ pascalize .ID }}({{ varname .Name }})
return {{ .ReceiverName }}
}
// Set{{ pascalize .ID }} adds the {{ camelize .Name }} to the {{ humanize $.Name }} params
func ({{ $.ReceiverName }} *{{ pascalize $.Name }}Params) Set{{ pascalize .ID }}({{ varname .Name }} {{ if and (not .IsArray) (not .IsMap) (not .HasDiscriminator) (not .IsStream) (or .IsNullable ) }}*{{ end }}{{ if not .IsFileParam }}{{ .GoType }}{{ else }}runtime.NamedReadCloser{{ end }}) {
{{ $.ReceiverName }}.{{ pascalize .ID }} = {{ varname .Name }}
}
{{- end }}
// WriteToRequest writes these params to a swagger request
func ({{ .ReceiverName }} *{{ pascalize .Name }}Params) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {
if err := r.SetTimeout({{ .ReceiverName }}.{{ camelize .TimeoutName }}); err != nil {
return err
}
var res []error
{{- range .Params }}
{{- if not (or .IsArray .IsMap .IsBodyParam) }}
{{- if and .IsNullable (not .AllowEmptyValue) }}
if {{ .ValueExpression }} != nil {
{{- end}}
{{- if .IsQueryParam }}
// query param {{ .Name }}
{{- if .IsNullable }}
var qr{{ pascalize .Name }} {{ .GoType }}
if {{ .ValueExpression }} != nil {
qr{{ pascalize .Name }} = *{{ .ValueExpression }}
}
{{- else }}
qr{{ pascalize .Name }} := {{ .ValueExpression }}
{{- end}}
q{{ pascalize .Name}} := {{ if .Formatter }}{{ .Formatter }}(qr{{ pascalize .Name }}){{ else }}qr{{ pascalize .Name }}{{ if .IsCustomFormatter }}.String(){{end}}{{end}}
{{- if not .AllowEmptyValue }}
if q{{ pascalize .Name }} != "" {
{{- end }}
if err := r.SetQueryParam({{ printf "%q" .Name }}, q{{ pascalize .Name }}); err != nil {
return err
}
{{- if not .AllowEmptyValue }}
}
{{- end }}
{{- else if .IsPathParam }}
// path param {{ .Name }}
if err := r.SetPathParam({{ printf "%q" .Name }}, {{ if .Formatter }}{{ .Formatter }}({{ if .IsNullable }}*{{end}}{{ .ValueExpression }}){{ else }}{{ if and (not .IsCustomFormatter) .IsNullable }}*{{end}}{{ .ValueExpression }}{{ if .IsCustomFormatter }}.String(){{end}}{{end}}); err != nil {
return err
}
{{- else if .IsHeaderParam }}
// header param {{ .Name }}
if err := r.SetHeaderParam({{ printf "%q" .Name }}, {{ if .Formatter }}{{ .Formatter }}({{ if .IsNullable }}*{{end}}{{ .ValueExpression }}){{ else }}{{ if and (not .IsCustomFormatter) .IsNullable }}*{{end}}{{ .ValueExpression }}{{ if .IsCustomFormatter }}.String(){{end}}{{end}}); err != nil {
return err
}
{{- else if .IsFormParam }}
{{- if .IsFileParam }}
{{- if .IsNullable }}
if {{ .ValueExpression }} != nil {
{{- end }}
// form file param {{ .Name }}
if err := r.SetFileParam({{ printf "%q" .Name }}, {{ .ValueExpression }}); err != nil {
return err
}
{{- if .IsNullable}}
}
{{- end }}
{{- else }}
// form param {{ .Name }}
{{- if .IsNullable }}
var fr{{ pascalize .Name }} {{ .GoType }}
if {{ .ValueExpression }} != nil {
fr{{ pascalize .Name }} = *{{ .ValueExpression }}
}
{{- else }}
fr{{ pascalize .Name }} := {{ .ValueExpression }}
{{- end}}
f{{ pascalize .Name}} := {{ if .Formatter }}{{ .Formatter }}(fr{{ pascalize .Name }}){{ else }}fr{{ pascalize .Name }}{{ if .IsCustomFormatter }}.String(){{end}}{{end}}
{{- if not .AllowEmptyValue }}
if f{{ pascalize .Name }} != "" {
{{- end }}
if err := r.SetFormParam({{ printf "%q" .Name }}, f{{ pascalize .Name }}); err != nil {
return err
}
{{- if not .AllowEmptyValue }}
}
{{- end }}
{{- end }}
{{- end }}
{{- if and .IsNullable (not .AllowEmptyValue) }}
}
{{- end }}
{{- else if .IsArray }}
{{- if not .IsBodyParam }}
if {{ .ValueExpression }} != nil {
{{- if .Child }}{{/* bind complex parameters (arrays and nested structures) */}}
// binding items for {{ .Name }}
joined{{ pascalize .Name }} := {{ .ReceiverName }}.bindParam{{ pascalize .Name }}(reg)
{{- else }}
values{{ pascalize .Name }} := {{ if and (not .IsArray) (not .IsStream) (not .IsMap) (.IsNullable) }}*{{end}}{{ .ValueExpression }}
joined{{ pascalize .Name}} := swag.JoinByFormat(values{{ pascalize .Name }}, "{{.CollectionFormat}}")
{{- end }}
{{- if .IsQueryParam }}
// query array param {{ .Name }}
if err := r.SetQueryParam({{ printf "%q" .Name }}, joined{{ pascalize .Name }}...); err != nil {
return err
}
{{- else if and .IsFormParam }}
// form array param {{ .Name }}
if err := r.SetFormParam({{ printf "%q" .Name }}, joined{{ pascalize .Name }}...); err != nil {
return err
}
{{- else if and .IsPathParam }}
// path array param {{ .Name }}
// SetPathParam does not support variadic arguments, since we used JoinByFormat
// we can send the first item in the array as it's all the items of the previous
// array joined together
if len(joined{{ pascalize .Name }}) > 0 {
if err := r.SetPathParam({{ printf "%q" .Name }}, joined{{ pascalize .Name }}[0]); err != nil {
return err
}
}
{{- else if .IsHeaderParam }}
// header array param {{ .Name }}
if len(joined{{ pascalize .Name }}) > 0 {
if err := r.SetHeaderParam({{ printf "%q" .Name }}, joined{{ pascalize .Name }}[0]); err != nil {
return err
}
}
{{- end }}
}
{{- end }}
{{- end }}
{{- if .IsBodyParam }}
{{- if or .Schema.IsInterface .Schema.IsStream (and .Schema.IsArray .Child) (and .Schema.IsMap .Child) (and .Schema.IsNullable (not .HasDiscriminator)) }}
if {{ .ValueExpression }} != nil {
{{- end }}
if err := r.SetBodyParam({{ .ValueExpression }}); err != nil {
return err
}
{{- if or .Schema.IsInterface .Schema.IsStream (and .Schema.IsArray .Child) (and .Schema.IsMap .Child) (and .Schema.IsNullable (not .HasDiscriminator)) }}
}
{{- end }}
{{- end }}
{{- end }}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
{{- range .Params }}
{{- if and (not .IsBodyParam) (not .IsFileParam) .IsArray }}
// bindParam{{ pascalize $.Name }} binds the parameter {{ .Name }}
func ({{ .ReceiverName }} *{{ pascalize $.Name }}Params) bindParam{{ pascalize .Name }}(formats strfmt.Registry) []string {
{{ varname .Child.ValueExpression }}R := {{ if and (not .IsArray) (not .IsStream) (not .IsMap) (.IsNullable) }}*{{end}}{{ .ValueExpression }}
{{ template "sliceclientparambinder" . }}
return {{ varname .Child.ValueExpression }}S
}
{{- end }}
{{- end }}
{{- define "sliceclientparambinder" }}
{{- if .IsArray }}
var {{ varname .Child.ValueExpression }}C []string
for _, {{ varname .Child.ValueExpression }}IR := range {{ varname .Child.ValueExpression }}R { // explode {{ .GoType }}
{{ template "sliceclientparambinder" .Child }}
{{ varname .Child.ValueExpression }}C = append({{ varname .Child.ValueExpression }}C, {{ varname .Child.ValueExpression }}IV)
}
// {{ .Child.ItemsDepth }}CollectionFormat: {{ printf "%q" .CollectionFormat }}
{{ varname .Child.ValueExpression }}S := swag.JoinByFormat({{ varname .Child.ValueExpression }}C, {{ printf "%q" .CollectionFormat }})
{{- if .Child.Parent }}{{/* NOTE: we cannot expect a "multi" CollectionFormat within an inner array */}}
{{ varname .Child.ValueExpression }}V := {{ varname .Child.ValueExpression }}S[0]
{{- end }}
{{- else }}
{{ varname .ValueExpression }}IV :=
{{- if .IsCustomFormatter }}
{{- print " " }}{{ varname .ValueExpression }}IR.String()
{{- else if eq .GoType "string" }}
{{- print " " }}{{ varname .ValueExpression }}IR
{{- else if .Formatter }}
{{- print " "}}{{ .Formatter }}({{ varname .ValueExpression }}IR)
{{- else }}
{{- print " " }}fmt.Sprintf("%v", {{ varname .ValueExpression }}IR)
{{- end }} // {{ .GoType }} as string
{{- end }}
{{- end }}

View file

@ -0,0 +1,346 @@
{{- define "clientresponse" }}
// New{{ pascalize .Name }} creates a {{ pascalize .Name }} with default headers values
func New{{ pascalize .Name }}({{ if eq .Code -1 }}code int{{ end }}{{ if .Schema }}{{ if and (eq .Code -1) .Schema.IsStream }}, {{end}}{{ if .Schema.IsStream }}writer io.Writer{{ end }}{{ end }}) *{{ pascalize .Name }} {
{{- if .Headers.HasSomeDefaults }}
var (
// initialize headers with default values
{{- range .Headers }}
{{- if .HasDefault }}
{{ template "simpleschemaDefaultsvar" . }}
{{- end }}
{{- end }}
)
{{- range .Headers }}
{{- if .HasDefault }}
{{ template "simpleschemaDefaultsinit" . }}
{{- end }}
{{- end }}
{{- end }}
return &{{ pascalize .Name }}{
{{- if eq .Code -1 }}
_statusCode: code,
{{- end }}
{{ range .Headers }}
{{- if .HasDefault }}
{{ pascalize .Name}}: {{ if and (not .IsArray) (not .HasDiscriminator) (not .IsInterface) (not .IsStream) .IsNullable }}&{{ end }}{{ varname .ID }}Default,
{{- end }}
{{- end }}
{{- if .Schema }}
{{- if .Schema.IsStream }}
Payload: writer,
{{- end }}
{{- end }}
}
}
/* {{ pascalize .Name}} describes a response with status code {{ .Code }}, with default header values.
{{ if .Description }}{{ blockcomment .Description }}{{else}}{{ pascalize .Name }} {{ humanize .Name }}{{end}}
*/
type {{ pascalize .Name }} struct {
{{- if eq .Code -1 }}
_statusCode int
{{- end }}
{{- range .Headers }}
{{- if .Description }}
/* {{ blockcomment .Description }}
{{- if or .SwaggerFormat .Default }}
{{ print "" }}
{{- if .SwaggerFormat }}
Format: {{ .SwaggerFormat }}
{{- end }}
{{- if .Default }}
Default: {{ json .Default }}
{{- end }}
{{- end }}
*/
{{- end }}
{{ pascalize .Name }} {{ .GoType }}
{{- end }}
{{- if .Schema }}
Payload {{ if and (not .Schema.IsBaseType) (not .Schema.IsInterface) .Schema.IsComplexObject (not .Schema.IsStream) }}*{{ end }}{{ if (not .Schema.IsStream) }}{{ .Schema.GoType }}{{ else }}io.Writer{{end}}
{{- end }}
}
// IsSuccess returns true when this {{ humanize .Name }} response has a 2xx status code
func ({{ .ReceiverName }} *{{ pascalize .Name }}) IsSuccess() bool {
{{- if eq .Code -1 }}
return {{ .ReceiverName }}._statusCode/100 == 2
{{- else }}
return {{ and (ge .Code 200) (lt .Code 300) }}
{{- end }}
}
// IsRedirect returns true when this {{ humanize .Name }} response has a 3xx status code
func ({{ .ReceiverName }} *{{ pascalize .Name }}) IsRedirect() bool {
{{- if eq .Code -1 }}
return {{ .ReceiverName }}._statusCode/100 == 3
{{- else }}
return {{ and (ge .Code 300) (lt .Code 400) }}
{{- end }}
}
// IsClientError returns true when this {{ humanize .Name }} response has a 4xx status code
func ({{ .ReceiverName }} *{{ pascalize .Name }}) IsClientError() bool {
{{- if eq .Code -1 }}
return {{ .ReceiverName }}._statusCode/100 == 4
{{- else }}
return {{ and (ge .Code 400) (lt .Code 500) }}
{{- end }}
}
// IsServerError returns true when this {{ humanize .Name }} response has a 5xx status code
func ({{ .ReceiverName }} *{{ pascalize .Name }}) IsServerError() bool {
{{- if eq .Code -1 }}
return {{ .ReceiverName }}._statusCode/100 == 5
{{- else }}
return {{ and (ge .Code 500) (lt .Code 600) }}
{{- end }}
}
// IsCode returns true when this {{ humanize .Name }} response a status code equal to that given
func ({{ .ReceiverName }} *{{ pascalize .Name }}) IsCode(code int) bool {
{{- if eq .Code -1 }}
return {{ .ReceiverName }}._statusCode == code
{{- else }}
return code == {{ .Code }}
{{- end }}
}
// Code gets the status code for the {{ humanize .Name }} response
func ({{ .ReceiverName }} *{{ pascalize .Name }}) Code() int {
{{- if eq .Code -1 }}
return {{ .ReceiverName }}._statusCode
{{- else }}
return {{ .Code }}
{{- end }}
}
func ({{ .ReceiverName }} *{{ pascalize .Name }}) Error() string {
return fmt.Sprintf("[{{ upper .Method }} {{ .Path }}][%d] {{ if .Name }}{{ .Name }} {{ else }}unknown error {{ end }}{{ if .Schema }} %+v{{ end }}", {{ if eq .Code -1 }}{{ .ReceiverName }}._statusCode{{ else }}{{ .Code }}{{ end }}{{ if .Schema }}, o.Payload{{ end }})
}
func ({{ .ReceiverName }} *{{ pascalize .Name }}) String() string {
return fmt.Sprintf("[{{ upper .Method }} {{ .Path }}][%d] {{ if .Name }}{{ .Name }} {{ else }}unknown response {{ end }}{{ if .Schema }} %+v{{ end }}", {{ if eq .Code -1 }}{{ .ReceiverName }}._statusCode{{ else }}{{ .Code }}{{ end }}{{ if .Schema }}, o.Payload{{ end }})
}
{{ if .Schema }}
func ({{ .ReceiverName }} *{{ pascalize .Name }}) GetPayload() {{ if and (not .Schema.IsBaseType) (not .Schema.IsInterface) .Schema.IsComplexObject (not .Schema.IsStream) }}*{{ end }}{{ if (not .Schema.IsStream) }}{{ .Schema.GoType }}{{ else }}io.Writer{{end}} {
return o.Payload
}
{{- end }}
func ({{ .ReceiverName }} *{{ pascalize .Name }}) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
{{- range .Headers }}
// hydrates response header {{.Name}}
hdr{{ pascalize .Name }} := response.GetHeader("{{ .Name }}")
if hdr{{ pascalize .Name }} != "" {
{{- if .Converter }}
val{{ camelize .Name }}, err := {{ .Converter }}(hdr{{ pascalize .Name }})
if err != nil {
return errors.InvalidType({{ .Path }}, "header", "{{ .GoType }}", hdr{{ pascalize .Name }})
}
{{ .ReceiverName }}.{{ pascalize .Name }} = val{{ camelize .Name }}
{{- else if .Child }}
// binding header items for {{ .Name }}
val{{ pascalize .Name }}, err := {{ .ReceiverName }}.bindHeader{{ pascalize .Name }}(hdr{{ pascalize .Name }}, formats)
if err != nil {
return err
}
{{ .ReceiverName }}.{{ pascalize .Name }} = val{{ pascalize .Name }}
{{- else if .IsCustomFormatter }}
val{{ camelize .Name }}, err := formats.Parse({{ printf "%q" .SwaggerFormat }}, hdr{{ pascalize .Name }})
if err != nil {
return errors.InvalidType({{ .Path }}, "header", "{{ .GoType }}", hdr{{ pascalize .Name }})
}
{{- if .IsNullable }}
v := (val{{ camelize .Name }}.({{ .GoType }}))
{{ .ReceiverName }}.{{ pascalize .Name }} = &v
{{- else }}
{{ .ReceiverName }}.{{ pascalize .Name }} = *(val{{ camelize .Name }}.(*{{ .GoType }}))
{{- end }}
{{- else }}
{{- if eq .GoType "string" }}
{{ .ReceiverName }}.{{ pascalize .Name }} = hdr{{ pascalize .Name }}
{{- else }}
{{ .ReceiverName }}.{{ pascalize .Name }} = {{ .GoType }}(hdr{{ pascalize .Name }})
{{- end }}
{{- end }}
}
{{- end }}
{{- if .Schema }}
{{- if .Schema.IsBaseType }}
// response payload as interface type
payload, err := {{ toPackageName .ModelsPackage }}.Unmarshal{{ dropPackage .Schema.GoType }}{{ if .Schema.IsArray}}Slice{{ end }}(response.Body(), consumer)
if err != nil {
return err
}
{{ .ReceiverName }}.Payload = payload
{{- else if .Schema.IsComplexObject }}
{{ .ReceiverName }}.Payload = new({{ .Schema.GoType }})
{{- end }}
{{- if not .Schema.IsBaseType }}
// response payload
if err := consumer.Consume(response.Body(), {{ if not (or .Schema.IsComplexObject .Schema.IsStream) }}&{{ end}}{{ .ReceiverName }}.Payload); err != nil && err != io.EOF {
return err
}
{{- end }}
{{- end }}
return nil
}
{{- range .Headers }}
{{- if .Child }}
// bindHeader{{ pascalize $.Name }} binds the response header {{ .Name }}
func ({{ .ReceiverName }} *{{ pascalize $.Name }}) bindHeader{{ pascalize .Name }}(hdr string, formats strfmt.Registry) ({{ .GoType }}, error) {
{{ varname .Child.ValueExpression }}V := hdr
{{ template "sliceclientheaderbinder" . }}
return {{ varname .Child.ValueExpression }}C, nil
}
{{- end }}
{{- end }}
{{- end }}
// Code generated by go-swagger; DO NOT EDIT.
{{ if .Copyright -}}// {{ comment .Copyright -}}{{ end }}
package {{ .Package }}
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"io"
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
{{ imports .DefaultImports }}
{{ imports .Imports }}
)
// {{ pascalize .Name }}Reader is a Reader for the {{ pascalize .Name }} structure.
type {{ pascalize .Name }}Reader struct {
formats strfmt.Registry
{{- if .HasStreamingResponse }}
writer io.Writer
{{- end }}
}
// ReadResponse reads a server response into the received {{ .ReceiverName }}.
func ({{ .ReceiverName }} *{{ pascalize .Name }}Reader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
{{- if .Responses}}
switch response.Code() {
{{- end }}
{{- range .Responses }}
case {{ .Code }}:
result := New{{ pascalize .Name }}({{ if .Schema }}{{ if .Schema.IsStream }}{{ $.ReceiverName }}.writer{{ end }}{{ end }})
if err := result.readResponse(response, consumer, {{ $.ReceiverName }}.formats); err != nil {
return nil, err
}
return {{ if .IsSuccess }}result, nil{{else}}nil, result{{ end }}
{{- end }}
{{- if .DefaultResponse }}
{{- with .DefaultResponse }}
{{- if $.Responses}}
default:
{{- end }}
result := New{{ pascalize .Name }}(response.Code(){{ if .Schema }}{{ if .Schema.IsStream }}, {{ $.ReceiverName }}.writer{{ end }}{{ end }})
if err := result.readResponse(response, consumer, {{ $.ReceiverName }}.formats); err != nil {
return nil, err
}
if response.Code() / 100 == 2 {
return result, nil
}
return nil, result
{{- end }}
{{- else }}
{{- if $.Responses}}
default:
{{- end }}
return nil, runtime.NewAPIError("[{{ upper .Method }} {{ .Path }}]{{ if .Name }} {{ .Name }}{{ end }}", response, response.Code())
{{- end }}
{{- if .Responses}}
}
{{- end }}
}
{{ range .Responses }}
{{ template "clientresponse" . }}
{{ end }}
{{ if .DefaultResponse }}
{{ template "clientresponse" .DefaultResponse }}
{{ end }}
{{ range .ExtraSchemas }}
/*{{ pascalize .Name }} {{ template "docstring" . }}
swagger:model {{ .Name }}
*/
{{- template "schema" . }}
{{- end }}
{{- define "sliceclientheaderbinder" }}
{{- if .IsArray }}
var (
{{ varname .Child.ValueExpression }}C {{ .GoType }}
)
// {{ .Child.ItemsDepth }}CollectionFormat: {{ printf "%q" .CollectionFormat }}
{{ varname .Child.ValueExpression }}R := swag.SplitByFormat({{ varname .Child.ValueExpression }}V, {{ printf "%q" .CollectionFormat }})
for {{ if or .Child.IsCustomFormatter .Child.Converter }}{{ .IndexVar }}{{ else }}_{{ end }}, {{ varname .Child.ValueExpression }}IV := range {{ varname .Child.ValueExpression }}R {
{{ template "sliceclientheaderbinder" .Child }}
{{ varname .Child.ValueExpression }}C = append({{ varname .Child.ValueExpression }}C, {{ varname .Child.ValueExpression }}IC) // roll-up {{ .Child.GoType }} into {{ .GoType }}
}
{{- else }}
// convert split string to {{ .GoType }}
{{- if .IsCustomFormatter }}
val, err := formats.Parse({{ printf "%q" .SwaggerFormat }}, {{ varname .ValueExpression }}IV)
if err != nil {
return nil, errors.InvalidType({{ .Path }}, "header{{ .ItemsDepth }}", "{{ .GoType }}", {{ varname .ValueExpression }}IV)
}
{{- if .IsNullable }}
{{ varname .ValueExpression }}IC := (&val).(*{{ .GoType }})
{{- else }}
{{ varname .ValueExpression }}IC := val.({{ .GoType }})
{{- end }}
{{- else if .Converter }}
val, err := {{- print " "}}{{ .Converter }}({{ varname .ValueExpression }}IV)
if err != nil {
return nil, errors.InvalidType({{ .Path }}, "header{{ .ItemsDepth }}", "{{ .GoType }}", {{ varname .ValueExpression }}IV)
}
{{- if .IsNullable }}
{{ varname .ValueExpression }}IC := &val
{{- else }}
{{ varname .ValueExpression }}IC := val
{{- end }}
{{- else }}
{{ varname .ValueExpression }}IC :=
{{- if eq .GoType "string" }}
{{- print " " }}{{ varname .ValueExpression }}IV
{{- else }}
{{- print " " }}fmt.Sprintf("%v", {{ varname .ValueExpression }}IV)
{{- end }} // string as {{ .GoType }}
{{- end }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,311 @@
# swagger
In Stratoscale, we really like the idea of API-first services, and we also really like Go.
We saw the go-swagger library, and thought that most of it can really help us. Generating code from
swagger files is a big problem with a lot of corner cases, and go-swagger is doing great job.
The one thing that we felt missing, is customization of the server to run with our design principles:
* Custom `main` function
* Dependency injection
* Limited scopes with unit testing.
Also:
* Adding you functions to the generated `configure_swagger_*.go` seems to be a burden.
* Lack of Interface that the service implement.
* Complicated and custom http clients and runtime.
Those are the changes that this contributor templates are providing:
## Server
### The new `restapi` package exposes interfaces
* Those interfaces can implemented by the developer and are the business logic of the service.
* The implementation of those is extensible.
* The implementation is separated from the generated code.
### The `restapi` returns an `http.Handler`
The `restapi.Handler` (see [example](./example/restapi/configure_swagger_petstore.go)) function returns
a standard `http.Handler`
* Given objects that implements the business logic, we can create a simple http handler.
* This handler is standard go http.Handler, so we can now use any other middleware, library, or framework
that support it.
* This handler is standard, so we understand it better.
## Client
* The new client package exposes interfaces, so functions in our code can receive those
interfaces which can be mocked for testing.
* The new client has a config that gets an `*url.URL` to customize the endpoint.
* The new client has a config that gets an `http.RoundTripper` to customize client with libraries, middleware or
frameworks that support the standard library's objects.
# Example Walk-Through
In the [example package](https://github.com/Stratoscale/swagger/tree/master/example) you'll find generated code and usage of the pet-store
[swagger file](./example/swagger.yaml).
* The `restapi`, `models` and `client` are auto-generated by the stratoscale/swagger docker file.
* The `internal` package was manually added and contains the server's business logic.
* The `main.go` file is the entrypoint and contains initializations and dependency injections of the project.
## Server
### [restapi](https://github.com/Stratoscale/swagger/tree/master/example/restapi)
This package is autogenerated and contains the server routing and parameters parsing.
The modified version contains `restapi.PetAPI` and `restapi.StoreAPI` which were auto generated.
```go
// PetAPI
type PetAPI interface {
PetCreate(ctx context.Context, params pet.PetCreateParams) middleware.Responder
PetDelete(ctx context.Context, params pet.PetDeleteParams) middleware.Responder
PetGet(ctx context.Context, params pet.PetGetParams) middleware.Responder
PetList(ctx context.Context, params pet.PetListParams) middleware.Responder
PetUpdate(ctx context.Context, params pet.PetUpdateParams) middleware.Responder
}
//go:generate mockery -name StoreAPI -inpkg
// StoreAPI
type StoreAPI interface {
InventoryGet(ctx context.Context, params store.InventoryGetParams) middleware.Responder
OrderCreate(ctx context.Context, params store.OrderCreateParams) middleware.Responder
// OrderDelete is For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors
OrderDelete(ctx context.Context, params store.OrderDeleteParams) middleware.Responder
// OrderGet is For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions
OrderGet(ctx context.Context, params store.OrderGetParams) middleware.Responder
}
```
Each function matches an `operationId` in the swagger file and they are grouped according to
the operation `tags`.
There is also a `restapi.Config`:
```go
// Config is configuration for Handler
type Config struct {
PetAPI
StoreAPI
Logger func(string, ...interface{})
// InnerMiddleware is for the handler executors. These do not apply to the swagger.json document.
// The middleware executes after routing but before authentication, binding and validation
InnerMiddleware func(http.Handler) http.Handler
}
```
This config is auto generated and contains all the declared interfaces above.
It is used to initiate an http.Handler with the `Handler` function:
```go
// Handler returns an http.Handler given the handler configuration
// It mounts all the business logic implementers in the right routing.
func Handler(c Config) (http.Handler, error) {
...
```
Let's look how we use this generated code to build our server.
### [internal](https://github.com/Stratoscale/swagger/tree/master/example/internal)
The `internal` package is **not** auto generated and contains the business logic of our server.
We can see two structs that implements the `restapi.PetAPI` and `restapi.StoreAPI` interfaces,
needed to make our server work.
When adding or removing functions from our REST API, we can just add or remove functions to those
business logic units. We can also create new logical units when they are added to our REST API.
### [main.go](./example/main.go)
The main function is pretty straight forward. We initiate our business logic units.
Then create a config for our rest API. We then create a standard `http.Handler` which we can
update with middleware, test with `httptest`, or to use with other standard tools.
The last piece is to run the handler with `http.ListenAndServe` or to use it with an `http.Server` -
it is all very customizable.
```go
func main() {
// Initiate business logic implementers.
// This is the main function, so here the implementers' dependencies can be
// injected, such as database, parameters from environment variables, or different
// clients for different APIs.
p := internal.Pet{}
s := internal.Store{}
// Initiate the http handler, with the objects that are implementing the business logic.
h, err := restapi.Handler(restapi.Config{
PetAPI: &p,
StoreAPI: &s,
Logger: log.Printf,
})
if err != nil {
log.Fatal(err)
}
// Run the standard http server
log.Fatal(http.ListenAndServe(":8080", h))
}
```
## Client
The client code is in the [client package](https://github.com/Stratoscale/swagger/tree/master/example/client) and is autogenerated.
To create a new client we use the `client.Config` struct:
```go
type Config struct {
// URL is the base URL of the upstream server
URL *url.URL
// Transport is an inner transport for the client
Transport http.RoundTripper
}
```
This enables us to use custom server endpoint or custom client middleware. Easily, with the
standard components, and with any library that accepts them.
The client is then generated with the New method:
```go
// New creates a new swagger petstore HTTP client.
func New(c Config) *SwaggerPetstore { ... }
```
This method returns an object that has two important fields:
```go
type SwaggerPetstore {
...
Pet *pet.Client
Store *store.Client
}
```
Thos fields are objects, which implements interfaces declared in the [pet](./example/client/pet) and
[store](./example/client/store) packages:
For example:
```go
// API is the interface of the pet client
type API interface {
// PetCreate adds a new pet to the store
PetCreate(ctx context.Context, params *PetCreateParams) (*PetCreateCreated, error)
// PetDelete deletes a pet
PetDelete(ctx context.Context, params *PetDeleteParams) (*PetDeleteNoContent, error)
// PetGet gets pet by it s ID
PetGet(ctx context.Context, params *PetGetParams) (*PetGetOK, error)
// PetList lists pets
PetList(ctx context.Context, params *PetListParams) (*PetListOK, error)
// PetUpdate updates an existing pet
PetUpdate(ctx context.Context, params *PetUpdateParams) (*PetUpdateCreated, error)
}
```
They are very similar to the server interfaces, and can be used by consumers of those APIs
(instead of using the actual client or the `*Pet` struct)
# Authentication
Authenticating and policy enforcement of the application is done in several stages, described below.
## Define security in swagger.yaml
Add to the root of the swagger.yaml the security and security definitions sections.
```yaml
securityDefinitions:
token:
type: apiKey
in: header
name: Cookie
security:
- token: []
```
The securityDefinitions section defines different security types that your application can handle.
The supported types by go-swagger are:
* `apiKey` - token that should be able to processed.
* `oauth2` - token and scopes that should be processed.
* and `basic` - user/password that should be processed.
Here we defined an apiKey, that is passed through the Cookie header.
The `security` section defines the default security enforcement for the application. You can select
different securityDefinitions, as the keys, and apply "scopes" as the values. Those default definitions
can be overriden in each route by a section with the same name:
```yaml
paths:
/pets:
post:
[...]
security:
- token: [admin]
```
Here we overriden the scope of token in the POST /pets URL so that only admin can use this API.
Let's see how we can use this functionality.
## Writing Security Handlers
Once we created a security definition named "token", a function called "AuthToken" was added to the `restapi.Config`:
```go
type Config struct {
...
// AuthToken Applies when the "Cookie" header is set
AuthToken func(token string) (interface{}, error)
}
```
This function gets the content of the Cookie header, and should return an `interface{}` and `error`.
The `interface{}` is the object that should represent the user that performed the request, it should
be nil to return an unauthorized 401 HTTP response. If the returned `error` is not nil, an HTTP 500,
internal server error will be returned.
The returned object, will be stored in the request context under the `restapi.AuthKey` key.
There is another function that we should know about, in the `restapi.Config` struct:
```go
type Config struct {
...
// Authorizer is used to authorize a request after the Auth function was called using the "Auth*" functions
// and the principal was stored in the context in the "AuthKey" context value.
Authorizer func(*http.Request) error
}
```
This one is a custom defined function that gets the request and can return an error.
If the returned error is not nil, and 403 HTTP error will be returned to the client - here the policy
enforcement comes to place.
There are two things that this function should be aware of:
1. The user - it can retrieve the user information from the context: `ctx.Value(restapi.AuthKey).(MyUserType)`.
Usually, a server will have a function for extracting this user information and returns a concrete
type which could be used by all the routes.
2. The route - it can retrieve the route using the go-swagger function: `middleware.MatchedRouteFrom(*http.Request)`.
So no need to parse URL and test the request method.
This route struct contains the route information. If for example, we want to check the scopes that were
defined for the current route in the swagger.yaml we can use the code below:
```go
for _, auth := range route.Authenticators {
for scopeName, scopeValues := range auth.Scopes {
for _, scopeValue := range scopeValues {
...
}
}
}
```

View file

@ -0,0 +1,111 @@
// Code generated by go-swagger; DO NOT EDIT.
{{ if .Copyright -}}// {{ comment .Copyright -}}{{ end }}
package {{ .Name }}
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"fmt"
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/swag"
"github.com/go-openapi/runtime"
"github.com/go-openapi/validate"
strfmt "github.com/go-openapi/strfmt"
{{ imports .DefaultImports }}
{{ imports .Imports }}
)
//go:generate mockery --name API --keeptree --with-expecter --case underscore
// API is the interface of the {{ humanize .Name }} client
type API interface {
{{ range .Operations -}}
/*
{{ pascalize .Name }} {{ if .Summary }}{{ pluralizeFirstWord (humanize .Summary) }}{{ if .Description }}
{{ blockcomment .Description }}{{ end }}{{ else if .Description}}{{ blockcomment .Description }}{{ else }}{{ humanize .Name }} API{{ end -}}
*/
{{ pascalize .Name }}(ctx context.Context, params *{{ pascalize .Name }}Params{{ if .HasStreamingResponse }}, writer io.Writer{{ end }}) {{ if .SuccessResponse }}({{ range .SuccessResponses }}*{{ pascalize .Name }}, {{ end }}{{ end }}error{{ if .SuccessResponse }}){{ end }}
{{ end -}}
}
// New creates a new {{ humanize .Name }} API client.
func New(transport runtime.ClientTransport, formats strfmt.Registry, authInfo runtime.ClientAuthInfoWriter) *Client {
return &Client{
transport: transport,
formats: formats,
authInfo: authInfo,
}
}
/*
Client {{ if .Summary }}{{ .Summary }}{{ if .Description }}
{{ blockcomment .Description }}{{ end }}{{ else if .Description}}{{ blockcomment .Description }}{{ else }}for {{ humanize .Name }} API{{ end }}
*/
type Client struct {
transport runtime.ClientTransport
formats strfmt.Registry
authInfo runtime.ClientAuthInfoWriter
}
{{ range .Operations -}}
/*
{{ pascalize .Name }} {{ if .Summary }}{{ pluralizeFirstWord (humanize .Summary) }}{{ if .Description }}
{{ blockcomment .Description }}{{ end }}{{ else if .Description}}{{ blockcomment .Description }}{{ else }}{{ humanize .Name }} API{{ end }}
*/
func (a *Client) {{ pascalize .Name }}(ctx context.Context, params *{{ pascalize .Name }}Params{{ if .HasStreamingResponse }}, writer io.Writer{{ end }}) {{ if .SuccessResponse }}({{ range .SuccessResponses }}*{{ pascalize .Name }}, {{ end }}{{ end }}error{{ if .SuccessResponse }}){{ end }} {
{{ $length := len .SuccessResponses }}
{{ $success := .SuccessResponses }}
{{ if .Responses }}result{{else}}_{{end}}, err := a.transport.Submit(&runtime.ClientOperation{
ID: {{ printf "%q" .Name }},
Method: {{ printf "%q" .Method }},
PathPattern: {{ printf "%q" .Path }},
ProducesMediaTypes: {{ printf "%#v" .ProducesMediaTypes }},
ConsumesMediaTypes: {{ printf "%#v" .ConsumesMediaTypes }},
Schemes: {{ printf "%#v" .Schemes }},
Params: params,
Reader: &{{ pascalize .Name }}Reader{formats: a.formats{{ if .HasStreamingResponse }}, writer: writer{{ end }}},
{{ if .Authorized -}}
AuthInfo: a.authInfo,
{{ end -}}
Context: ctx,
Client: params.HTTPClient,
})
if err != nil {
return {{ if $success }}{{ padSurround "nil" "nil" 0 $length }}, {{ end }}err
}
{{- if .Responses }}
switch value := result.(type) {
{{- range $i, $v := .Responses }}
case *{{ pascalize $v.Name }}:
{{- if $v.IsSuccess }}
return {{ if $success }}{{ padSurround "value" "nil" $i $length }},{{ end }}nil
{{- else }}
return {{ if $success }}{{ padSurround "nil" "nil" 0 $length }},{{ end }}runtime.NewAPIError("unsuccessful response", value, value.Code())
{{- end }}
{{- end }}
}
{{- if .DefaultResponse }}
// unexpected success response
unexpectedSuccess := result.(*{{ pascalize .DefaultResponse.Name }})
return {{ if $success }}{{ padSurround "nil" "nil" 0 $length }}, {{ end }}runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code())
{{- else }}
// safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue
msg := fmt.Sprintf("unexpected success response for {{ .Name }}: API contract not enforced by server. Client expected to get an error, but got: %T", result)
panic(msg)
{{- end }}
{{- else }}
return nil
{{- end }}
}
{{ end }}

View file

@ -0,0 +1,83 @@
// Code generated by go-swagger; DO NOT EDIT.
{{ if .Copyright -}}// {{ comment .Copyright -}}{{ end }}
package {{ .Package }}
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/url"
"net/http"
rtclient "github.com/go-openapi/runtime/client"
"github.com/go-openapi/swag"
"github.com/go-openapi/spec"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/strfmt"
{{ imports .DefaultImports }}
{{ imports .Imports }}
)
const (
// DefaultHost is the default Host
// found in Meta (info) section of spec file
DefaultHost string = {{ printf "%#v" .Host }}
// DefaultBasePath is the default BasePath
// found in Meta (info) section of spec file
DefaultBasePath string = {{ printf "%#v" .BasePath }}
)
// DefaultSchemes are the default schemes found in Meta (info) section of spec file
var DefaultSchemes = {{ printf "%#v" .Schemes }}
type Config struct {
// URL is the base URL of the upstream server
URL *url.URL
// Transport is an inner transport for the client
Transport http.RoundTripper
// AuthInfo is for authentication
AuthInfo runtime.ClientAuthInfoWriter
}
// New creates a new {{ humanize .Name }} HTTP client.
func New(c Config) *{{ pascalize .Name }} {
var (
host = DefaultHost
basePath = DefaultBasePath
schemes = DefaultSchemes
)
if c.URL != nil {
host = c.URL.Host
basePath = c.URL.Path
schemes = []string{c.URL.Scheme}
}
transport := rtclient.New(host, basePath, schemes)
if c.Transport != nil {
transport.Transport = c.Transport
}
cli := new({{ pascalize .Name }})
cli.Transport = transport
{{ range .OperationGroups -}}
cli.{{ pascalize .Name }} = {{ .PackageAlias }}.New(transport, strfmt.Default, c.AuthInfo)
{{ end -}}
return cli
}
// {{ pascalize .Name }} is a client for {{ humanize .Name }}
type {{ pascalize .Name }} struct {
{{ range .OperationGroups -}}
{{ pascalize .Name }} {{ .PackageAlias }}.API
{{ end -}}
Transport runtime.ClientTransport
}

View file

@ -0,0 +1,222 @@
// Code generated by go-swagger; DO NOT EDIT.
{{ if .Copyright -}}// {{ comment .Copyright -}}{{ end }}
package {{ .APIPackage }}
import (
"context"
"crypto/tls"
"net/http"
"log"
"fmt"
"github.com/go-openapi/errors"
"github.com/go-openapi/loads"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/runtime/security"
{{ imports .DefaultImports }}
{{ imports .Imports }}
)
{{ $package := .Package }}
type contextKey string
const AuthKey contextKey = "Auth"
{{ range .OperationGroups -}}
//go:generate mockery -name {{ pascalize .Name}}API -inpkg
/* {{ pascalize .Name }}API {{ .Description }} */
type {{ pascalize .Name }}API interface {
{{ range .Operations -}}
{{ if .Summary -}}
/* {{ pascalize .Name }} {{ .Summary }} */
{{ else if .Description -}}
/* {{ pascalize .Name }} {{ .Description }} */
{{ end -}}
{{ pascalize .Name }}(ctx context.Context, params {{.Package}}.{{ pascalize .Name }}Params) middleware.Responder
{{ end -}}
}
{{ end }}
// Config is configuration for Handler
type Config struct {
{{ range .OperationGroups -}}
{{ pascalize .Name }}API
{{ end -}}
Logger func(string, ...interface{})
// InnerMiddleware is for the handler executors. These do not apply to the swagger.json document.
// The middleware executes after routing but before authentication, binding and validation
InnerMiddleware func(http.Handler) http.Handler
// Authorizer is used to authorize a request after the Auth function was called using the "Auth*" functions
// and the principal was stored in the context in the "AuthKey" context value.
Authorizer func(*http.Request) error
{{ range .SecurityDefinitions -}}
{{ if .IsBasicAuth -}}
// Auth{{ pascalize .ID }} for basic authentication
Auth{{ pascalize .ID }} func(user string, pass string) ({{ if .PrincipalIsNullable }}*{{ end }}{{ .Principal }}, error)
{{ end -}}
{{ if .IsAPIKeyAuth -}}
// Auth{{ pascalize .ID }} Applies when the "{{ .Name }}" {{ .Source }} is set
Auth{{ pascalize .ID }} func(token string) ({{ if .PrincipalIsNullable }}*{{ end }}{{ .Principal }}, error)
{{ end }}
{{ if .IsOAuth2 -}}
// Auth{{ pascalize .ID }} For OAuth2 authentication
Auth{{ pascalize .ID }} func(token string, scopes []string) ({{ if .PrincipalIsNullable }}*{{ end }}{{ .Principal }}, error)
{{ end -}}
{{ end -}}
// Authenticator to use for all APIKey authentication
APIKeyAuthenticator func(string, string, security.TokenAuthentication) runtime.Authenticator
// Authenticator to use for all Bearer authentication
BasicAuthenticator func(security.UserPassAuthentication) runtime.Authenticator
// Authenticator to use for all Basic authentication
BearerAuthenticator func(string, security.ScopedTokenAuthentication) runtime.Authenticator
{{ range .Consumes -}}
{{ if .Implementation -}}
// {{ pascalize .Name }}Consumer is a {{ .Name }} consumer that will replace the default if not nil.
{{ pascalize .Name }}Consumer runtime.Consumer
{{ end -}}
{{ end -}}
}
// Handler returns an http.Handler given the handler configuration
// It mounts all the business logic implementers in the right routing.
func Handler(c Config) (http.Handler, error) {
h, _, err := HandlerAPI(c)
return h, err
}
// HandlerAPI returns an http.Handler given the handler configuration
// and the corresponding *{{ pascalize .Name }} instance.
// It mounts all the business logic implementers in the right routing.
func HandlerAPI(c Config) (http.Handler, *{{.Package}}.{{ pascalize .Name }}API, error) {
spec, err := loads.Analyzed(swaggerCopy(SwaggerJSON), "")
if err != nil {
return nil, nil, fmt.Errorf("analyze swagger: %v", err)
}
api := {{.Package}}.New{{ pascalize .Name }}API(spec)
api.ServeError = errors.ServeError
api.Logger = c.Logger
if c.APIKeyAuthenticator != nil {
api.APIKeyAuthenticator = c.APIKeyAuthenticator
}
if c.BasicAuthenticator != nil {
api.BasicAuthenticator = c.BasicAuthenticator
}
if c.BearerAuthenticator != nil {
api.BearerAuthenticator = c.BearerAuthenticator
}
{{ range .Consumes -}}
if c.{{ pascalize .Name }}Consumer != nil {
api.{{ pascalize .Name }}Consumer = c.{{ pascalize .Name }}Consumer
} else {
{{ if .Implementation -}}
api.{{ pascalize .Name }}Consumer = {{ .Implementation }}
{{ else }}
api.{{ pascalize .Name }}Consumer = runtime.ConsumerFunc(func(r io.Reader, target interface{}) error {
return errors.NotImplemented("{{.Name}} consumer has not yet been implemented")
})
{{ end -}}
}
{{ end -}}
{{ range .Produces -}}
{{ if .Implementation -}}
api.{{ pascalize .Name }}Producer = {{ .Implementation }}
{{ else -}}
api.{{ pascalize .Name }}Producer = runtime.ProducerFunc(func(w io.Writer, data interface{}) error {
return errors.NotImplemented("{{.Name}} producer has not yet been implemented")
})
{{ end -}}
{{ end -}}
{{ range .SecurityDefinitions -}}
{{ if .IsBasicAuth -}}
api.{{ pascalize .ID }}Auth = func(user string, pass string) ({{if .PrincipalIsNullable }}*{{ end }}{{.Principal}}, error) {
if c.Auth{{ pascalize .ID }} == nil {
{{- if eq .Principal "interface{}" }}
return "", nil
{{- else }}
panic("you specified a custom principal type, but did not provide the authenticator to provide this")
{{- end }}
}
return c.Auth{{ pascalize .ID }}(user, pass)
}
{{ end -}}
{{ if .IsAPIKeyAuth -}}
api.{{ pascalize .ID }}Auth = func(token string) ({{ if .PrincipalIsNullable }}*{{ end }}{{.Principal}}, error) {
if c.Auth{{ pascalize .ID }} == nil {
{{- if eq .Principal "interface{}" }}
return token, nil
{{- else }}
panic("you specified a custom principal type, but did not provide the authenticator to provide this")
{{- end }}
}
return c.Auth{{ pascalize .ID }}(token)
}
{{ end }}
{{ if .IsOAuth2 -}}
api.{{ pascalize .ID }}Auth = func(token string, scopes []string) ({{ if .PrincipalIsNullable }}*{{ end }}{{.Principal}}, error) {
if c.Auth{{ pascalize .ID }} == nil {
{{- if eq .Principal "interface{}" }}
return token, nil
{{- else }}
panic("you specified a custom principal type, but did not provide the authenticator to provide this")
{{- end }}
}
return c.Auth{{ pascalize .ID }}(token, scopes)
}
{{ end -}}
{{ end -}}
{{ if .SecurityDefinitions -}}
api.APIAuthorizer = authorizer(c.Authorizer)
{{ end -}}
{{ range .Operations -}}
api.{{if ne .Package $package}}{{pascalize .Package}}{{end}}{{ pascalize .Name }}Handler =
{{- .PackageAlias }}.{{ pascalize .Name }}HandlerFunc(func(params {{.PackageAlias}}.{{ pascalize .Name }}Params{{if .Authorized}}, principal {{ if .PrincipalIsNullable }}*{{ end }}{{ .Principal }}{{end}}) middleware.Responder {
ctx := params.HTTPRequest.Context()
{{ if .Authorized -}}
ctx = storeAuth(ctx, principal)
{{ end -}}
return c.{{pascalize .Package}}API.{{pascalize .Name}}(ctx, params)
})
{{ end -}}
api.ServerShutdown = func() { }
return api.Serve(c.InnerMiddleware), api, nil
}
// swaggerCopy copies the swagger json to prevent data races in runtime
func swaggerCopy(orig json.RawMessage) json.RawMessage {
c := make(json.RawMessage, len(orig))
copy(c, orig)
return c
}
// authorizer is a helper function to implement the runtime.Authorizer interface.
type authorizer func(*http.Request) error
func (a authorizer) Authorize(req *http.Request, principal interface{}) error {
if a == nil {
return nil
}
ctx := storeAuth(req.Context(), principal)
return a(req.WithContext(ctx))
}
func storeAuth(ctx context.Context, principal interface{}) context.Context {
return context.WithValue(ctx, AuthKey, principal)
}

View file

@ -0,0 +1,9 @@
// Code generated by go-swagger; DO NOT EDIT.
{{ if .Copyright -}}// {{ comment .Copyright -}}{{ end }}
package {{ .APIPackage }}
// this file is intentionally empty. Otherwise go-swagger will generate a server which we don't want

View file

@ -0,0 +1,25 @@
{{ define "docstring" }}
{{- if .Title }}
{{- comment .Title }}
{{- if .Description }}
//
// {{ comment .Description }}
{{- end }}
{{- else if .Description}}
{{- comment .Description }}
{{- else }}
{{- humanize .Name }}
{{- end }}
{{- if or .MinProperties .MinProperties }}
//
{{- if .MinProperties }}
// Min Properties: {{ .MinProperties }}
{{- end }}
{{- if .MaxProperties }}
// Max Properties: {{ .MaxProperties }}
{{- end }}
{{- end }}
{{- if .Example }}
// Example: {{ print .Example }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,20 @@
// Code generated by go-swagger; DO NOT EDIT.
{{ if .Copyright -}}
// {{ comment .Copyright }}
{{- end }}
package {{.Package}}
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/strfmt"
{{- if .DefaultImports }}
{{ imports .DefaultImports }}
{{- end }}
{{- if .Imports }}
{{ imports .Imports }}
{{- end }}
)

View file

@ -0,0 +1,527 @@
{{- define "externalDoc" }}{{/* renders external documentation */}}
{{- with .ExternalDocs }}
{{- if .URL }}
{{- if .Description }}
> [{{ trimSpace .Description }}]({{ .URL }})
{{- else }}
> [Read more]({{ .URL }})
{{- end }}
{{- else }}
> {{ trimSpace .Description }}
{{- end }}
{{- end }}
{{- end }}
{{- define "docParam" }}{{/* renders a parameter with simple schema */}}
| {{ .Name }} | `{{ .Location }}` | {{ paramDocType . }} | `{{ .GoType }}` | {{ if .CollectionFormat }}`{{ docCollectionFormat .CollectionFormat .Child }}`{{ end }} | {{ if .Required }}{{ end }} | {{ if .Default }}`{{ json .Default }}`{{ end }} | {{ trimSpace .Description }} |
{{- end }}
{{- define "docModelSchema" }}{{/* renders a schema */}}
{{- if .IsArray }}
{{- if .IsAliased }}
[{{- dropPackage .GoType }}](#{{ dasherize (dropPackage .GoType) -}})
{{- else if .Items }}
{{- if and .Items.IsPrimitive (not .Items.IsAliased) -}}
{{- schemaDocType . -}}
{{- else -}}
[][{{- dropPackage .Items.GoType }}](#{{ dasherize (dropPackage .Items.GoType) -}})
{{- end -}}
{{- else -}}
[]any{{ printf " " -}}
{{- end -}}
{{- else if and .IsMap (not .IsAdditionalProperties) -}}
{{- if .IsAliased -}}
[{{- dropPackage .GoType }}](#{{ dasherize (dropPackage .GoType) -}})
{{- else if .ElemType }}
{{- if and .ElemType.IsPrimitive (not .ElemType.IsAliased) (not .ElemType.IsInterface) -}}
{{ schemaDocMapType . -}}
{{- else if .ElemType.IsInterface -}}
map of any{{ printf " " -}}
{{- else -}}
map of [{{- dropPackage .ElemType.GoType }}](#{{ dasherize (dropPackage .ElemType.GoType) -}})
{{- end -}}
{{- else -}}
map of any{{ printf " " -}}
{{- end -}}
{{- else if and .IsAliased .IsPrimitive (not .IsSuperAlias) -}}
| Name | Type | Go type | Default | Description | Example |
|------|------|---------| ------- |-------------|---------|
| {{ .Name }} | {{ schemaDocType . }}| {{ .AliasedType }} | {{ if .Default }}`{{ json .Default }}`{{ end }}| {{ trimSpace .Description }} | {{ if .Example }}`{{ .Example }}`{{ end }} |
{{ printf "\n" }}
{{- else if or (and .IsAliased (not (.IsAdditionalProperties))) (and .IsComplexObject (not .Properties) (not .AllOf)) -}}
[{{- dropPackage .GoType }}](#{{ dasherize (dropPackage .GoType) -}})
{{- else if and .IsInterface (not .IsAliased) (not .IsMap) -}}
any
{{- else -}}
{{- range .AllOf }}
{{- if .IsAnonymous }}
* inlined member (*{{ .Name }}*)
{{ template "docModelSchema" . }}
{{- else if or .IsComplexObject .IsPrimitive }}
* composed type [{{- dropPackage .GoType }}](#{{ dasherize (dropPackage .GoType) -}})
{{- else }}
* {{ template "docModelSchema" . }}
{{- end }}
{{- end }}
{{- if .Properties }}
**{{ if .IsTuple }}Tuple members{{ else }}Properties{{ end }}**
| Name | Type | Go type | Required | Default | Description | Example |
|------|------|---------|:--------:| ------- |-------------|---------|
{{- range .Properties }}
| {{ .Name }} | {{ template "docSchemaSimple" . }}| `{{ .GoType }}` | {{ if .Required }}{{ end }} | {{ if .Default }}`{{ json .Default }}`{{ end }}| {{ trimSpace .Description }} | {{ if .Example }}`{{ .Example }}`{{ end }} |
{{- end }}
{{ printf "\n" }}
{{- end }}
{{- if .HasAdditionalProperties }}
**Additional Properties**
{{- with .AdditionalProperties }}
{{- if .IsInterface }}
any
{{- else if .IsPrimitive }}
| Type | Go type | Default | Description | Example |
|------|---------| ------- |-------------|---------|
| {{ template "docSchemaSimple" . }} | `{{ .GoType }}` |{{ if .Default }}`{{ json .Default }}`{{ end }}| {{ trimSpace .Description }} | {{ if .Example }}`{{ .Example }}`{{ end }} |
{{- else }}
{{ template "docModelSchema" . }}
{{- end }}
{{- end }}
{{- end }}
{{- if and .IsTuple .HasAdditionalItems }}
{{- with .AdditionalItems }}
**Additional Items**
{{- if .IsInterface }}
any
{{- else if .IsPrimitive }}
| Type | Go type | Default | Description | Example |
|------|---------| ------- |-------------|---------|
| {{ template "docSchemaSimple" . }} | `{{ .GoType }}` |{{ if .Default }}`{{ json .Default }}`{{ end }}| {{ trimSpace .Description }} | {{ if .Example }}`{{ .Example }}`{{ end }} |
{{- else }}
{{ template "docModelSchema" . }}
{{- end }}
{{- end }}
{{- end }}
{{- end -}}
{{- end }}
{{- define "docModel" }}{{/* renders a definition */}}
{{- with .Description }}
> {{ .}}
{{- end }}
{{- if .ExternalDocs }}
{{ template "externalDoc" . }}
{{- end }}
{{ if or .Description .ExternalDocs }}
{{ printf "\n" }}
{{- end }}
{{ template "docModelSchema" .}}
{{- end }}
{{- define "docSchemaSimple" }}{{/* renders a simple property */}}
{{- if .IsAliased -}}
[{{- dropPackage .GoType }}](#{{ dasherize (dropPackage .GoType) -}})
{{- else if .IsArray }}
{{- if .Items }}
{{- if and .Items.IsPrimitive (not .Items.IsAliased) -}}
{{- schemaDocType . -}}
{{- else -}}
[][{{- dropPackage .Items.GoType }}](#{{ dasherize (dropPackage .Items.GoType) -}})
{{- end -}}
{{- else -}}
[]any{{ printf " " -}}
{{- end -}}
{{- else if .IsMap -}}
{{- if .ElemType }}
{{- if and .ElemType.IsPrimitive (not .ElemType.IsAliased) (not .ElemType.IsInterface) -}}
{{ schemaDocMapType . -}}
{{- else if .ElemType.IsInterface -}}
map of any{{ printf " " -}}
{{- else -}}
map of [{{- dropPackage .ElemType.GoType }}](#{{ dasherize (dropPackage .ElemType.GoType) -}})
{{- end -}}
{{- else -}}
map of any{{ printf " " -}}
{{- end -}}
{{- else if .IsPrimitive -}}
{{- schemaDocType . -}}
{{- else -}}
[{{- dropPackage .GoType }}](#{{ dasherize (dropPackage .GoType) -}})
{{- end -}}
{{- end }}
{{- define "docModelBodyParam" }}{{/* layout for body param schema */}}
| {{ .Name }} | `body` | {{ template "docSchemaSimple" .Schema }} | `{{ .Schema.GoType }}` | | {{ if .Required }}{{ end }} | {{ if .Default }}`{{ json .Default }}`{{ end }}| {{ trimSpace .Description }} |
{{- end }}
{{- define "docHeaders" }}{{/* renders response headers */}}
{{- if .Headers }}
| Name | Type | Go type | Separator | Default | Description |
|------|------|---------|-----------|---------|-------------|
{{- range .Headers }}
| {{ .Name }} | {{ headerDocType . }} | `{{ .GoType }}` | {{ if .CollectionFormat }}`{{ docCollectionFormat .CollectionFormat .Child }}`{{ end }} | {{ if .Default }}`{{ json .Default }}`{{ end }} | {{ trimSpace .Description }} |
{{- end }}
{{- end }}
{{- end }}
{{/* spec top-level information block */}}
{{- if .Info }}
{{- with .Info.Title }}
# {{ . }}
{{- end }}
{{- with .Info.Description }}
{{ . }}
{{- end }}
{{ template "externalDoc" . }}
{{- if or .Info.Version .Info.License .Info.Contact .Info.TermsOfService }}
## Informations
{{- end }}
{{- with .Info.Version }}
### Version
{{ . }}
{{- end }}
{{- with .Info.License }}
### License
{{ if .Name }}[{{ .Name }}]({{ end}}{{ .URL }}{{ if .Name }}){{ end }}
{{- end }}
{{- with .Info.Contact }}
### Contact
{{ .Name }} {{ .Email }} {{ .URL }}
{{- end }}
{{- with .Info.TermsOfService }}
### Terms Of Service
{{ . }}
{{- end }}
{{- else }}
{{ template "externalDoc" . }}
{{- end }}
{{- if .Tags }}
## Tags
{{- range .Tags }}
### <span id="tag-{{ dasherize .Name }}"></span>{{ if .ExternalDocs }}[{{ .Name }}]({{ .ExternalDocs.URL }}{{ if .ExternalDocs.Description }} {{ printf "%q" .ExternalDocs.Description }}{{ end }}){{ else }}{{ .Name }}{{ end }}
{{- if .Description }}
{{ .Description }}
{{- end }}
{{- end }}
{{- end }}
{{- if or .Schemes .Consumes .Produces }}
## Content negotiation
{{- end }}
{{- if .Schemes }}
### URI Schemes
{{- range .Schemes }}
* {{ . }}
{{- end }}
{{- range .ExtraSchemes }}
* {{ . }}
{{- end }}
{{- end }}
{{- if .Consumes }}
### Consumes
{{- range .Consumes }}
{{- range .AllSerializers }}
* {{ .MediaType }}
{{- end }}
{{- end }}
{{- end }}{{/* end .Schemes */}}
{{- if .Produces }}
### Produces
{{- range .Produces }}
{{- range .AllSerializers }}
* {{ .MediaType }}
{{- end }}
{{- end }}
{{- end }}
{{- if or .SecurityDefinitions .SecurityRequirements }}
## Access control
{{- end }}
{{- if .SecurityDefinitions }}
### Security Schemes
{{- range .SecurityDefinitions }}
#### {{ .ID }}{{ if .Source }} ({{ .Source }}{{ with .Name }}: {{ . }}{{ end }}){{ end }}
{{ .Description }}
{{- with .Type }}
> **Type**: {{ . }}
{{- end }}
{{- if .IsOAuth2}}
{{- with .Flow }}
>
> **Flow**: {{ . }}
{{- end }}
{{- with .AuthorizationURL }}
>
> **Authorization URL**: {{ . }}
{{- end }}
{{- with .TokenURL }}
>
> **Token URL**: {{ . }}
{{- end }}
{{ if .ScopesDesc }}
##### Scopes
Name | Description
-----|-------------
{{- range .ScopesDesc }}
{{ .Name }} | {{ .Description }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}{{/* end .SecurityDefinitions */}}
{{- if .SecurityRequirements }}
### Security Requirements
{{- range .SecurityRequirements }}
* {{ .Name }}{{ if .Scopes }}: {{ range $idx, $scope := .Scopes }}{{ if gt $idx 0 }}, {{ end }}{{ $scope }}{{ end }}
{{- end }}
{{- end }}
{{- end }}{{/* end .SecurityRequirements */}}
## All endpoints{{/* an index of all API endpoints */}}
{{- $alltags := .Tags }}
{{- range .OperationGroups }}
### {{ .PackageAlias }}
{{- $pkg := .PackageAlias }}
{{- range $alltags }}
{{- if eq .Name $pkg }}
{{ template "externalDoc" . }}
{{- end }}
{{- end }}
| Method | URI | Name | Summary |
|---------|---------|--------|---------|
{{- range .Operations }}
| {{ upper .Method }} | {{ joinPath .BasePath .Path }} | [{{ humanize .Name }}](#{{ dasherize .Name }}) | {{ .Summary }} |
{{- end }}
{{ printf "\n" }}
{{- end }}
## Paths{{/* all paths to operations */}}
{{- range .Operations }}
{{- $opname := .Name }}
### <span id="{{ dasherize .Name }}"></span> {{ if .Summary }}{{ trimSpace .Summary }}{{ else }}{{ humanize .Name }}{{ end }} (*{{ .Name }}*)
```
{{ upper .Method }} {{ joinPath .BasePath .Path }}
```
{{- with .Description }}
{{ . }}
{{- end }}
{{- with .ExternalDocs }}
> {{ if .URL }}[Read more]({{ .URL }} "{{ .Description }}"){{ end }}
{{- end }}
{{- if or (gt (len .SchemeOverrides) 0) (gt (len .ExtraSchemeOverrides) 0) }}
#### URI Schemes
{{- range .SchemeOverrides }}
* {{ . }}
{{- end }}
{{- range .ExtraSchemeOverrides }}
* {{ . }}
{{- end }}
{{- end }}
{{- if .Consumes }}
#### Consumes
{{- range .Consumes }}
* {{ . }}
{{- end }}
{{- end }}
{{- if .Produces }}
#### Produces
{{- range .Produces }}
* {{ . }}
{{- end }}
{{- end }}
{{- if .SecurityRequirements }}
#### Security Requirements
{{- range .SecurityRequirements }}
* {{ .Name }}{{ if .Scopes }}: {{ range $idx, $scope := .Scopes }}{{ if gt $idx 0 }}, {{ end }}{{ $scope }}{{ end }}{{ end }}
{{- end }}
{{- end }}
{{- if .Params }}
#### Parameters
| Name | Source | Type | Go type | Separator | Required | Default | Description |
|------|--------|------|---------|-----------| :------: |---------|-------------|
{{- range .PathParams }}{{ template "docParam" . }}{{ end }}
{{- range .HeaderParams }}{{ template "docParam" . }}{{ end }}
{{- range .QueryParams }}{{ template "docParam" . }}{{ end }}
{{- range .FormParams }}{{ template "docParam" . }}{{ end }}
{{- range .Params }}
{{- if .IsBodyParam }}
{{- template "docModelBodyParam" . }}
{{- end }}
{{- end }}
{{- end }}{{/* end .Params */}}
#### All responses
| Code | Status | Description | Has headers | Schema |
|------|--------|-------------|:-----------:|--------|
{{- range .Responses }}
| [{{.Code}}](#{{ dasherize $opname }}-{{ .Code }}) | {{ httpStatus .Code }} | {{ trimSpace .Description }} | {{ if .Headers }}{{ end }} | [schema](#{{ dasherize $opname }}-{{ .Code }}-schema) |
{{- end }}
{{- with .DefaultResponse }}
| [default](#{{ dasherize $opname }}-default) | | {{ trimSpace .Description }} | {{ if .Headers }}{{ end }} | [schema](#{{ dasherize $opname }}-default-schema) |
{{- end }}
#### Responses
{{ range .Responses }}
##### <span id="{{ dasherize $opname }}-{{ .Code }}"></span> {{.Code}}{{ if .Description }} - {{ trimSpace .Description }}{{ end }}
Status: {{ httpStatus .Code }}
###### <span id="{{ dasherize $opname }}-{{ .Code }}-schema"></span> Schema
{{- if .Schema }}
{{ template "docModel" .Schema }}
{{- end }}
{{- if .Examples }}
###### Examples
{{ range .Examples }}
**{{ .MediaType }}**
```json
{{ prettyjson .Example }}
```
{{- end }}
{{- end }}
{{- if .Headers }}
###### Response headers
{{ template "docHeaders" . }}
{{- end }}
{{- end }}
{{- with .DefaultResponse }}
##### <span id="{{ dasherize $opname }}-default"></span> Default Response
{{ trimSpace .Description }}
###### <span id="{{ dasherize $opname }}-default-schema"></span> Schema
{{- if .Schema }}
{{ template "docModel" .Schema }}
{{- else }}
empty schema
{{- end }}
{{- if .Examples }}
###### Examples
{{ range .Examples }}
**{{ .MediaType }}**
```json
{{ .Example }}
```
{{- end }}
{{- end }}
{{- if .Headers }}
###### Response headers
{{ template "docHeaders" . }}
{{- end }}
{{- end }}
{{- if .ExtraSchemas }}
###### Inlined models
{{- range .ExtraSchemas }}
{{- if ne .Name "" }}
**<span id="{{ dasherize .Name }}"></span> {{ .Name }}**
{{ template "docModel" . }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}{{/* end .Operations */}}
## Models
{{- range .Models }}
### <span id="{{ dasherize .Name }}"></span> {{ .Name }}
{{ template "docModel" . }}
{{- if .ExtraSchemas }}
#### Inlined models
{{- range .ExtraSchemas }}
{{- if ne .Name "" }}
**<span id="{{ dasherize .Name }}"></span> {{ .Name }}**
{{ template "docModel" . }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,27 @@
{{ template "header" . }}
{{- if .IncludeModel }}
{{- if .IsExported }}
// {{ pascalize .Name }} {{ template "docstring" . }}
{{- template "annotations" . }}
{{- end }}
{{- template "schema" . }}
{{- end }}
{{ range .ExtraSchemas }}
{{- if .IncludeModel }}
{{- if .IsExported }}
// {{ pascalize .Name }} {{ template "docstring" . }}
{{- template "annotations" . }}
{{- end }}
{{- template "schema" . }}
{{- end }}
{{- end }}
{{- define "annotations" }}{{/* annotations to generate spec from source */}}
{{- if not .IsBaseType }}
//
// swagger:model {{ .Name }}
{{- else }}
//
// swagger:discriminator {{ .Name }} {{ .DiscriminatorField }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,131 @@
{{- if and .IsBaseType .IsExported (not .IsSuperAlias) }}
{{- template "schemaPolymorphic" . }}
{{- else if .IsSuperAlias }}
type {{ pascalize .Name }} {{ template "typeSchemaType" . }}{{/* For types declared as $ref on some other type, just declare the type as a golang _aliased_ type, e.g. type A = B. No method shall be redeclared. */}}
{{- if .IsBaseType }}
{{ template "baseTypeSerializer" . }}{{/* When the alias redeclares a polymorphic type, define factory methods with this alias. */}}
{{- end }}
{{- else if .IsEmbedded }}
{{- template "schemaEmbedded" . }}
{{- else }}
{{- if or .IsComplexObject .IsTuple .IsAdditionalProperties }}{{/* TODO(fred): handle case of subtype inheriting from base type with AdditionalProperties, issue #2220 */}}
{{ if .Name }}type {{ if not .IsExported }}{{ .Name }}{{ else }}{{ pascalize .Name }}{{ end }}{{ end }} {{ template "schemaBody" . }}
{{- range .Properties }}
{{- if .IsBaseType }}
// {{ pascalize .Name}} gets the {{ humanize .Name }} of this base type{{/* all properties which are of a base type propagate its interface */}}
func ({{ $.ReceiverName}} *{{ pascalize $.Name}}) {{ pascalize .Name}}() {{ template "schemaType" . }}{
{{- if eq $.DiscriminatorField .Name }}
return {{ printf "%q" $.DiscriminatorValue }}
{{- else }}
return {{ $.ReceiverName }}.{{camelize .Name}}Field
{{- end }}
}
// Set{{ pascalize .Name}} sets the {{ humanize .Name }} of this base type
func ({{ $.ReceiverName}} *{{ pascalize $.Name}}) Set{{ pascalize .Name}}(val {{ template "schemaType" . }}) {
{{- if ne $.DiscriminatorField .Name }}
{{ $.ReceiverName }}.{{camelize .Name}}Field = val
{{- end }}
}
{{- end }}
{{- end }}
{{- if .Default }}{{/* TODO(fred) - issue #2189 */}}
func ({{.ReceiverName}} *{{ pascalize .Name }}) UnmarshalJSON(b []byte) error {
type {{ pascalize .Name }}Alias {{ pascalize .Name }}
var t {{ pascalize .Name }}Alias
if err := json.Unmarshal([]byte({{printf "%q" (json .Default)}}), &t); err != nil {
return err
}
if err := json.Unmarshal(b, &t); err != nil {
return err
}
*{{.ReceiverName}} = {{ pascalize .Name }}(t)
return nil
}
{{- end }}
{{- else }}
type {{ pascalize .Name }} {{ template "typeSchemaType" . }}
{{- end }}
{{- if (and .IsPrimitive .IsAliased .IsCustomFormatter (not (stringContains .Zero "(\""))) }}
{{ template "aliasedSerializer" . }}
{{- end }}
{{- if .IsSubType }}
{{ range .AllOf }}
{{ range .Properties }}
{{- if .IsBaseType }}
// {{ pascalize .Name}} gets the {{ humanize .Name }} of this subtype
func ({{$.ReceiverName}} *{{ pascalize $.Name}}) {{ pascalize .Name}}() {{ template "schemaType" . }}{
{{- if eq $.DiscriminatorField .Name }}
return {{ printf "%q" $.DiscriminatorValue }}
{{- else }}
return {{ $.ReceiverName }}.{{camelize .Name}}Field
{{- end }}
}
// Set{{ pascalize .Name}} sets the {{ humanize .Name }} of this subtype
func ({{$.ReceiverName}} *{{ pascalize $.Name}}) Set{{ pascalize .Name}}(val {{ template "schemaType" . }}) {
{{- if ne $.DiscriminatorField .Name }}
{{ $.ReceiverName }}.{{camelize .Name}}Field = val
{{- end }}
}
{{- end }}
{{- end }}{{/* TODO(fred): handle AdditionalProperties in base type */}}
{{- end }}
{{ template "mapOrSliceGetter" . }}
{{- end }}
{{ template "schemaSerializer" . }}
{{- end }}
{{- if and .IncludeValidator (not .IsSuperAlias) (not .IsEmbedded) }}{{/* aliased types type A = B do not redefine methods */}}
{{- if and (not (or .IsInterface .IsStream)) (or .Required .HasValidations .HasBaseType) }}
{{- if (eq .SwaggerType "string") }}{{/* Enum factory for enums for which we generate const (atm, only strings)*/}}
{{- if .Enum }}
func New{{ pascalize .Name }}(value {{ .GoType }}) *{{ .GoType }} {
return &value
}
// Pointer returns a pointer to a freshly-allocated {{ .GoType }}.
func ({{ .ReceiverName }} {{ .GoType }}) Pointer() *{{ .GoType }} {
return &{{ .ReceiverName }}
}
{{- end }}
{{- end }}
{{ template "schemavalidator" . }}
{{- else if not (or .IsInterface .IsStream) }}
// Validate validates this {{ humanize .Name }}{{/* this schema implements the runtime.Validatable interface but has no validations to check */}}
func ({{.ReceiverName}} {{ if or .IsTuple .IsComplexObject .IsAdditionalProperties }}*{{ end }}{{ if or (not .IsExported) .Discriminates }}{{ camelize .Name }}{{ else }}{{ pascalize .Name }}{{ end }}) Validate(formats strfmt.Registry) error {
return nil
}
{{- else }}{{/* {{ .Name }} does not implement the runtime.Validatable interface: noop */}}
{{- end }}
{{- if and (not (or .IsInterface .IsStream)) (or .HasContextValidations) }}
{{ template "schemacontextvalidator" . }}
{{- else if not (or .IsInterface .IsStream) }}
// ContextValidate validates this {{ humanize .Name }} based on context it is used {{/* this schema implements the runtime.ContextValidatable interface but has no validations to check */}}
func ({{.ReceiverName}} {{ if or .IsTuple .IsComplexObject .IsAdditionalProperties }}*{{ end }}{{ if or (not .IsExported) .Discriminates }}{{ camelize .Name }}{{ else }}{{ pascalize .Name }}{{ end }}) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
{{- else }}{{/* {{ .Name }} does not implement the runtime.Validatable interface: noop */}}
{{- end }}
{{- end }}
{{- if .WantsMarshalBinary }}
{{ template "marshalBinarySerializer" . }}
{{- end }}
{{- define "mapOrSliceGetter" }}{{/* signature for AdditionalProperties and AdditionalItems getter funcs */}}
{{- if not .IsBaseType }}
{{- if .HasAdditionalProperties }}
{{- with .AdditionalProperties }}
// {{- template "docstring" . }}{{- template "propertyValidationDocString" . }}
{{ pascalize .Name }}() map[string]{{ template "schemaType" . }}
{{- end }}
{{- end }}
{{- with .AdditionalItems }}
// {{- template "docstring" . }}{{- template "propertyValidationDocString" . }}
{{ pascalize .Name }}() []{{ template "schemaType" . }}
{{- end }}
{{- else }}
// AdditionalProperties in base type shoud be handled just like regular properties{{/* TODO(fred): add full support for AdditionalProperties in base type */}}
// At this moment, the base type property is pushed down to the subtype
{{- end }}
{{- end }}

View file

@ -0,0 +1,330 @@
{{ define "schemaBody" }}struct {
{{ range .AllOf }}
{{ if or (and $.IsSubType .IsBaseType .IsExported) .IsAnonymous }}
{{ range .Properties }}
{{ if ne $.DiscriminatorField .Name }}
{{ if or (not $.IsExported) (and $.IsSubType .IsBaseType) }}
{{ if $.IsTuple }}
{{ template "privtuplefield" . }}
{{ else }}
{{template "privstructfield" . }}
{{ end }}
{{ else }}
{{ if $.IsTuple }}
{{ template "tuplefield" . }}
{{ else }}
{{template "structfield" . }}
{{ end }}
{{ end}}
{{ end }}
{{ end }}
{{- if .HasAdditionalProperties }}
{{- if .AdditionalProperties }}
// {{ template "docstring" .AdditionalProperties }}
{{- template "propertyValidationDocString" .AdditionalProperties}}
{{- if and .IsExported (not .IsSubType) }}
{{ pascalize .AdditionalProperties.Name }}
{{- else if or (not .AdditionalProperties.IsExported) (.AdditionalProperties.IsBaseType) }}
{{ camelize .AdditionalProperties.Name }}Field
{{- else }}
{{ .AdditionalProperties.Name }}
{{- end }} map[string]{{ template "schemaType" .AdditionalProperties }} `json:"-"`
{{- end }}
{{- end }}
{{- if .AdditionalItems }}
// {{ template "docstring" .AdditionalItems }}
{{- template "propertyValidationDocString" .AdditionalItems}}
{{- if and .IsExported (not $.IsSubType) }}{{/* TODO(fred): make sure inherited AdditionalItems are camelized */}}
{{ pascalize .AdditionalItems.Name }}
{{- else }}
{{ .AdditionalItems.Name }}
{{- end }} []{{ template "schemaType" .AdditionalItems }} `json:"-"`
{{- end }}
{{ else }}{{/* named type composition */}}
{{ if not (and $.IsBaseType .IsExported) }}{{ .GoType }}{{ end }}
{{ end }}
{{ end }}
{{range .Properties}}
{{ if or (not $.IsExported) ($.IsBaseType) (.IsBaseType) }}
{{ if $.IsTuple }}{{ template "privtuplefield" . }}{{ else }}{{template "privstructfield" . }}{{ end }}{{ else }}{{ if $.IsTuple }}{{ template "tuplefield" . }}{{ else }}{{template "structfield" . }}{{ end }}{{ end}}
{{ end }}
{{ if .HasAdditionalProperties }}
{{- if .AdditionalProperties }}
// {{ template "docstring" .AdditionalProperties }}
{{- template "propertyValidationDocString" .AdditionalProperties}}
{{- if and .IsExported (not .IsSubType) }}
{{ pascalize .AdditionalProperties.Name }}
{{- else }}
{{ pascalize .AdditionalProperties.Name }}Field
{{- end }} map[string]{{ template "schemaType" .AdditionalProperties }} `json:"-"`
{{ end }}
{{- end }}
{{- if .AdditionalItems }}
// {{ template "docstring" .AdditionalItems }}
{{- template "propertyValidationDocString" .AdditionalItems}}
{{ if and .IsExported (not .IsSubType) }}{{ pascalize .AdditionalItems.Name }}{{ else }}{{ pascalize .AdditionalItems.Name }}Field{{ end }} []{{ template "schemaType" .AdditionalItems }} `json:"-"`
{{ end }}
}
{{- end }}
{{ define "subTypeBody" }}struct {
{{- range .AllOf }}
{{- if or (and .IsBaseType .IsExported) .IsAnonymous }}
{{- range .Properties }}
{{- if not $.IsExported }}
{{- if $.IsTuple }}
{{- template "privtuplefield" . }}
{{- else }}
{{- template "privstructfield" . }}
{{- end }}
{{- else }}
{{- if $.IsTuple }}
{{- template "tuplefield" . }}
{{- else }}
{{- template "structfield" . }}
{{- end }}
{{- end }}
{{- end }}
{{- if .HasAdditionalProperties }}
{{- if .AdditionalProperties }}
{{- if .IsExported }}
{{ pascalize .AdditionalProperties.Name }}
{{- else }}
{{ .AdditionalProperties.Name }}
{{- end }} map[string]{{ template "schemaType" .AdditionalProperties }} `json:"-"`
{{- end }}
{{- end }}
{{- if .AdditionalItems }}
{{- if .IsExported }}
{{ pascalize .AdditionalItems.Name }}
{{- else }}
{{ .AdditionalItems.Name }}
{{- end }} []{{ template "schemaType" .AdditionalItems }} `json:"-"`
{{- end }}
{{- else }}
{{- if not (and .IsBaseType .IsExported) }}
{{ .GoType }}
{{- end }}
{{- end }}
{{- end }}
{{ range .Properties }}
{{- if not $.IsExported }}
{{- if $.IsTuple }}
{{ template "privtuplefield" . }}
{{- else }}
{{ template "privstructfield" . }}
{{- end }}
{{- else }}
{{- if $.IsTuple }}
{{ template "tuplefield" . }}
{{- else }}
{{ template "structfield" . }}
{{- end }}
{{- end}}
{{- end }}
{{- if .HasAdditionalProperties }}
{{- if .AdditionalProperties }}
{{- if and .IsExported }}
{{ pascalize .AdditionalProperties.Name }}
{{- else }}
{{ pascalize .AdditionalProperties.Name }}Field
{{- end }} map[string]{{ template "schemaType" .AdditionalProperties }} `json:"-"`
{{- end }}
{{- end }}
{{- if .AdditionalItems }}
{{- if and .IsExported (not .IsSubType) }}
{{ pascalize .AdditionalItems.Name }}
{{- else }}
{{ pascalize .AdditionalItems.Name }}Field
{{- end }} []{{ template "schemaType" .AdditionalItems }} `json:"-"`
{{- end }}
}
{{- end }}
{{ define "withBaseTypeBody" }}struct {
{{ range .AllOf }}
{{ if or (and .IsBaseType .IsExported) .IsAnonymous }}{{ range .Properties }}
{{ if not .IsExported }}{{ if .IsTuple }}{{ template "privtuplefield" . }}{{ else }}{{template "privstructfield" . }}{{ end }}{{ else }}{{ if $.IsTuple }}{{ template "tuplefield" . }}{{ else }}{{template "structfield" . }}{{ end }}{{ end}}
{{ end }}{{ if .HasAdditionalProperties }}{{ if .IsExported }}{{ pascalize .AdditionalProperties.Name }}{{ else }}{{ .AdditionalProperties.Name }}{{ end }} map[string]{{ template "schemaType" .AdditionalProperties }} `json:"-"` {{end}}
{{ if .AdditionalItems }}{{ if and .IsExported }}{{ pascalize .AdditionalItems.Name }}{{ else }}{{ .AdditionalItems.Name }}{{ end }} []{{ template "schemaType" .AdditionalItems }} `json:"-"`
{{ end }}
{{ else }}
{{ if not (and .IsBaseType .IsExported) }}{{ .GoType }}{{ end }}{{ end }}
{{ end }}
{{range .Properties}}{{ if .IsBaseType }}
{{ if not $.IsExported }}{{ else }}{{ pascalize .Name}} {{ template "schemaType" . }} `json:"{{ .Name }}{{ if and (not .Required) .IsEmptyOmitted }},omitempty{{ end }}{{ if .IsJSONString }},string{{ end }}"`{{ end}}
{{end}}{{ end }}
{{ if .HasAdditionalProperties }}{{ if and .IsExported }}{{ pascalize .AdditionalProperties.Name }}{{ else }}{{ pascalize .AdditionalProperties.Name }}Field{{ end }} map[string]{{ template "schemaType" .AdditionalProperties }} `json:"-"`
{{ end }}
{{ if .AdditionalItems }}{{ if and .IsExported (not .IsSubType) }}{{ pascalize .AdditionalItems.Name }}{{ else }}{{ pascalize .AdditionalItems.Name }}Field{{ end }} []{{ template "schemaType" .AdditionalItems }} `json:"-"`
{{ end }}
}
{{- end }}
{{ define "withoutBaseTypeBody" }}struct {
{{ range .AllOf }}
{{ if .IsAnonymous }}
{{ range .Properties }}
{{ if and .IsExported (not .IsBaseType) }}
{{ if .IsTuple }}
{{ template "tuplefield" . }}
{{ else }}
{{template "structfield" . }}
{{ end }}
{{ else }}
{{ pascalize .Name }} json.RawMessage `json:"{{ .Name }}{{ if and (not .Required) .IsEmptyOmitted }},omitempty{{ end }}{{ if .IsJSONString }},string{{ end }}"`
{{ end}}
{{ end }}
{{ if .HasAdditionalProperties }}
{{ if .AdditionalProperties }}
{{ if .IsExported }}{{ pascalize .AdditionalProperties.Name }}{{ else }}{{ .AdditionalProperties.Name }}{{ end }} map[string]{{ template "schemaType" .AdditionalProperties }} `json:"-"`
{{end}}
{{ end }}
{{ if .AdditionalItems }}
{{ if .IsExported }}{{ pascalize .AdditionalItems.Name }}{{ else }}{{ .AdditionalItems.Name }}{{ end }} []{{ template "schemaType" .AdditionalItems }} `json:"-"`
{{ end }}
{{ else }}
{{ if not (and .IsBaseType .IsExported) }}
{{ .GoType }}
{{ end }}
{{ end }}
{{ end }}
{{range .Properties}}
{{ if not .IsBaseType }}
{{ if not $.IsExported }}
{{template "privstructfield" . }}
{{ else }}
{{ pascalize .Name}} {{ template "schemaType" . }} `json:"{{ .Name }}{{ if and (not .Required) .IsEmptyOmitted }},omitempty{{ end }}{{ if .IsJSONString }},string{{ end }}"`
{{ end}}
{{ else }}
{{ pascalize .Name }} json.RawMessage `json:"{{ .Name }}{{ if and (not .Required) .IsEmptyOmitted }},omitempty{{ end }}{{ if .IsJSONString }},string{{ end }}"`
{{end}}
{{ end }}
{{ if .HasAdditionalProperties }}
{{ pascalize .AdditionalProperties.Name }}{{ if .IsExported }}Field{{ end }} map[string]{{ template "schemaType" .AdditionalProperties }} `json:"-"`
{{ end }}
}
{{- end }}
{{ define "withoutBaseTypeBodyOrNonExported" }}struct {
{{ range .AllOf }}
{{ if .IsAnonymous }}
{{ range .Properties }}
{{ if and .IsExported (not .IsBaseType) }}
{{ if .IsTuple }}
{{ template "tuplefield" . }}
{{ else }}
{{template "structfield" . }}
{{ end }}
{{ end}}
{{ end }}
{{ if .HasAdditionalProperties }}
{{ if .AdditionalProperties }}
{{ if .IsExported }}{{ pascalize .AdditionalProperties.Name }}{{ else }}{{ .AdditionalProperties.Name }}{{ end }} map[string]{{ template "schemaType" .AdditionalProperties }} `json:"-"`
{{end}}
{{ end }}
{{ if .AdditionalItems }}
{{ if .IsExported }}{{ pascalize .AdditionalItems.Name }}{{ else }}{{ .AdditionalItems.Name }}{{ end }} []{{ template "schemaType" .AdditionalItems }} `json:"-"`
{{ end }}
{{ else }}
{{ if not (and .IsBaseType .IsExported) }}
{{ .GoType }}
{{ end }}
{{ end }}
{{ end }}
{{range .Properties}}
{{ if not .IsBaseType }}
{{ if not .IsExported }}
{{template "privstructfield" . }}
{{ else }}
{{ pascalize .Name}} {{ template "schemaType" . }} `json:"{{ .Name }}{{ if and (not .Required) .IsEmptyOmitted }},omitempty{{ end }}{{ if .IsJSONString }},string{{ end }}"`
{{ end}}
{{end}}
{{ end }}
{{ if .HasAdditionalProperties }}
{{ pascalize .AdditionalProperties.Name }}{{ if .IsExported }}Field{{ end }} map[string]{{ template "schemaType" .AdditionalProperties }} `json:"-"`
{{ end }}}{
{{ range .AllOf }}
{{ if .IsAnonymous }}
{{ range .Properties }}
{{ if not .IsBaseType }}
{{ pascalize .Name }}: {{ .ReceiverName}}.{{ pascalize .Name }},
{{ end }}
{{ end }}
{{ else }}
{{ if not (and .IsBaseType .IsExported) }}
{{ .GoType }}: {{ .ReceiverName }}.{{ .GoType }},
{{ end }}
{{ end }}
{{ end }}
{{ range .Properties }}
{{ if and (not .IsBaseType) .IsExported }}
{{ pascalize .Name }}: {{ .ReceiverName }}.{{ pascalize .Name }},
{{ end }}
{{ end }}
},
{{- end }}
{{ define "withBaseTypeBodyAndNonExported" }}struct{
{{ range .AllOf }}
{{ range .Properties }}
{{ if .IsBaseType }}
{{ pascalize .Name }} {{ template "schemaType" . }} `json:"{{ .Name }}{{ if and (not .Required) .IsEmptyOmitted }},omitempty{{ end }}{{ if .IsJSONString }},string{{ end }}"`
{{ end }}
{{ end }}
{{ end }}
{{ range .Properties }}
{{ if or (not .IsExported) .IsBaseType }}
{{ pascalize .Name }} {{ template "schemaType" . }} `json:"{{ .Name }}{{ if and (not .Required) .IsEmptyOmitted }},omitempty{{ end }}{{ if .IsJSONString }},string{{ end }}"`
{{ end }}
{{end}}} {
{{ range .AllOf }}
{{ range .Properties }}
{{ if .IsBaseType }}
{{ pascalize .Name }}:
{{ if ne .DiscriminatorField .Name }}
{{ .ReceiverName }}.{{ if .IsSubType}}{{ camelize .Name }}Field{{ else }}{{ pascalize .Name }}(){{ end }},
{{ else }}
{{ .ReceiverName }}.{{pascalize .Name}}(),
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{ range .Properties }}
{{ if or (not .IsExported) .IsBaseType }}
{{ pascalize .Name }}: {{ .ReceiverName }}.{{ if .IsBaseType}}{{ camelize .Name }}Field{{ else }}{{ pascalize .Name }}{{ end }},
{{ end }}
{{ end }} },
{{- end }}
{{ define "withoutAdditionalBody" }}struct {
{{ range .AllOf }}
{{ if or (and $.IsSubType .IsBaseType .IsExported) .IsAnonymous }}{{ range .Properties }}
{{ if ne $.DiscriminatorField .Name }}{{ if or (not $.IsExported) (and $.IsSubType .IsBaseType) }}{{ if $.IsTuple }}{{ template "privtuplefield" . }}{{ else }}{{template "privstructfield" . }}{{ end }}{{ else }}{{ if $.IsTuple }}{{ template "tuplefield" . }}{{ else }}{{template "structfield" . }}{{ end }}{{ end}}{{ end }}
{{ end }}
{{ else }}
{{ if not (and .IsBaseType .IsExported) }}{{ .GoType }}{{ end }}{{ end }}
{{ end }}
{{range .Properties}}
{{ if or (not $.IsExported) (and $.IsSubType .IsBaseType) }}{{ if $.IsTuple }}{{ template "privtuplefield" . }}{{ else }}{{template "privstructfield" . }}{{ end }}{{ else }}{{ if $.IsTuple }}{{ template "tuplefield" . }}{{ else }}{{template "structfield" . }}{{ end }}{{ end}}
{{end}}
}
{{- end }}
{{ define "JustBaseTypeBody" }}struct {
/* Just the base type fields. Used for unmashalling polymorphic types.*/
{{ range .AllOf }}
{{ if .IsBaseType }}
{{ range .Properties }}
{{ if .IsExported }}
{{ if .IsTuple }}
{{ template "tuplefield" . }}
{{ else }}
{{template "structfield" . }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
}
{{- end }}

View file

@ -0,0 +1,21 @@
{{ define "schemaEmbedded" }}
type {{ pascalize .Name }} struct {
{{ if .ElemType.IsNullable }}*{{ end }}{{ .ElemType.GoType }}
}
func ({{.ReceiverName }} {{ if or .IsTuple .IsComplexObject }}*{{ end }}{{ if .Discriminates }}{{ camelize .Name }}{{ else if .IsExported }}{{ pascalize .Name }}{{ else }}{{ .Name }}{{ end }}) Validate(formats strfmt.Registry) error {
var f interface{} = {{ .ReceiverName }}.{{ dropPackage .ElemType.GoType }}
if v, ok := f.(runtime.Validatable) ; ok {
return v.Validate(formats)
}
return nil
}
func ({{.ReceiverName }} {{ if or .IsTuple .IsComplexObject }}*{{ end }}{{ if .Discriminates }}{{ camelize .Name }}{{ else if .IsExported }}{{ pascalize .Name }}{{ else }}{{ .Name }}{{ end }}) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var f interface{} = {{ .ReceiverName }}.{{ dropPackage .ElemType.GoType }}
if v, ok := f.(runtime.ContextValidatable) ; ok {
return v.ContextValidate(ctx, formats)
}
return nil
}
{{- end }}

View file

@ -0,0 +1,53 @@
{{ define "schemaPolymorphic" }}
type {{ pascalize .Name }} interface {
{{- if not (or .IsInterface .IsStream) }}{{/*
A base type is always Validatable.
Under normal conditions, we can't have a base type rendered a .IsStream or .IsInterface: this check is just for sanity check).
In the definition of the base type itself, this means that the unexported struct holding
the definition of the base type has a Validate() func and a ContextValitate() func.
*/}}
runtime.Validatable
runtime.ContextValidatable
{{- end }}
{{ range .AllOf }}
{{- if .IsAnonymous }}
{{ range .Properties }}
{{ if $.IsTuple }}{{ template "tuplefieldIface" . }}{{ else }}{{template "structfieldIface" . }}{{ end }}
{{- end }}
{{ template "mapOrSliceGetter" . }}
{{- else }}
{{ .GoType }}
{{- end }}
{{- end }}
{{ range .Properties }}
{{- if $.IsTuple }}
{{ template "tuplefieldIface" . }}
{{- else }}
{{ template "structfieldIface" . }}
{{- end }}
{{- end }}
{{ template "mapOrSliceGetter" . }}
}
type {{ camelize .Name }} {{ template "schemaBody" . }}{{/* unexported implementation of the interface (TODO(fred): atm, this is not used, issue #232) */}}
{{- range .Properties }}
// {{ pascalize .Name}} gets the {{ humanize .Name }} of this polymorphic type
func ({{ $.ReceiverName}} *{{ camelize $.Name}}) {{ pascalize .Name}}() {{ template "schemaType" . }}{
{{- if eq $.DiscriminatorField .Name }}
return {{ printf "%q" $.DiscriminatorValue }}
{{- else }}
return {{ $.ReceiverName }}.{{camelize .Name}}Field
{{- end }}
}
// Set{{ pascalize .Name}} sets the {{ humanize .Name }} of this polymorphic type
func ({{ $.ReceiverName}} *{{ camelize $.Name}}) Set{{ pascalize .Name}}(val {{ template "schemaType" . }}) {
{{- if ne $.DiscriminatorField .Name }}
{{ $.ReceiverName }}.{{camelize .Name}}Field = val
{{- end }}
}
{{- end }}{{/* TODO(fred): AdditionalProperties */}}
{{ template "polymorphicSerializer" . }}
{{- end }}

View file

@ -0,0 +1,29 @@
{{ define "schemaType" }}
{{- if and (or (gt (len .AllOf) 0) .IsAnonymous) ( not .IsMap) }}
{{- template "schemaBody" . }}
{{- else }}
{{- if and (not .IsMap) .IsNullable (not .IsSuperAlias) }}*{{ end }}
{{- if .IsSuperAlias }} = {{ end }}
{{- .GoType }}
{{- end}}
{{- end }}
{{ define "dereffedSchemaType" }}
{{- if and (or (gt (len .AllOf) 0) .IsAnonymous) ( not .IsMap) }}
{{- template "schemaBody" . }}
{{- else }}
{{- .GoType }}
{{- end}}
{{- end }}
{{ define "typeSchemaType" }}
{{- if and (or (gt (len .AllOf) 0) .IsAnonymous) ( not .IsMap) ( not .IsSuperAlias ) }}
{{- template "schemaBody" . }}
{{- else if and .IsSubType ( not .IsSuperAlias ) }}
{{- template "subTypeBody" . }}
{{- else }}
{{- if and (not .IsMap) .IsNullable (not .IsSuperAlias) }}*{{ end }}
{{- if .IsSuperAlias }} = {{ end }}
{{- if .AliasedType }}{{ .AliasedType }}{{ else }}{{ .GoType }}{{ end }}
{{- end}}
{{- end }}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,94 @@
{{ define "additionalPropertiesSerializer" }}
// UnmarshalJSON unmarshals this object with additional properties from JSON
func ({{.ReceiverName}} *{{ pascalize .Name }}) UnmarshalJSON(data []byte) error {
// stage 1, bind the properties
var stage1 {{ template "withoutAdditionalBody" . }}
if err := json.Unmarshal(data, &stage1); err != nil {
return err
}
var rcv {{ pascalize .Name }}
{{ range .Properties }}
rcv.{{ pascalize .Name }} = stage1.{{ pascalize .Name }}
{{- end }}
*{{ .ReceiverName }} = rcv
// stage 2, remove properties and add to map
stage2 := make(map[string]{{ if .AdditionalProperties }}json.RawMessage{{ else }}interface{}{{ end }})
if err := json.Unmarshal(data, &stage2); err != nil {
return err
}
{{ range .Properties }}
delete(stage2, {{ printf "%q" .Name }})
{{- end }}
{{- if .AdditionalProperties }}
// stage 3, add additional properties values
if len(stage2) > 0 {
result := make(map[string]{{ template "schemaType" .AdditionalProperties }})
for k, v := range stage2 {
var toadd {{ template "schemaType" .AdditionalProperties }}
if err := json.Unmarshal(v, {{if not .AdditionalProperties.IsNullable }}&{{ end }}toadd); err != nil {
return err
}
result[k] = toadd
}
{{ .ValueExpression }} = result
}
{{- else }}
{{ .ValueExpression }} = stage2
{{- end }}
return nil
}
// MarshalJSON marshals this object with additional properties into a JSON object
func ({{.ReceiverName}} {{ pascalize .Name }}) MarshalJSON() ([]byte, error) {
var stage1 {{ template "withoutAdditionalBody" . }}
{{ range .Properties }}
stage1.{{ pascalize .Name }} = {{ .ValueExpression }}
{{- end }}
// make JSON object for known properties
props, err := json.Marshal(stage1)
if err != nil {
return nil, err
}
if len({{ .ValueExpression }}) == 0 { // no additional properties
return props, nil
}
// make JSON object for the additional properties
additional, err := json.Marshal({{ .ValueExpression }})
if err != nil {
return nil, err
}
if len(props) < 3 { // "{}": only additional properties
return additional, nil
}
// concatenate the 2 objects
return swag.ConcatJSON(props, additional), nil
}
{{- end }}
{{ define "noAdditionalPropertiesSerializer" }}
// UnmarshalJSON unmarshals this object while disallowing additional properties from JSON
func ({{.ReceiverName}} *{{ pascalize .Name }}) UnmarshalJSON(data []byte) error {
var props {{ template "withoutAdditionalBody" . }}
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
if err := dec.Decode(&props); err != nil {
return err
}
{{- $rcv := .ReceiverName }}
{{ range .Properties }}
{{ .ReceiverName }}.{{ pascalize .Name }} = props.{{ pascalize .Name }}
{{- end }}
return nil
}
{{- end }}

Some files were not shown because too many files have changed in this diff Show more