gotosocial/vendor/github.com/go-swagger/go-swagger/generator/spec.go
tobi fd8a724e77
[chore] Bump go swagger (#2871)
* bump go swagger version

* bump swagger version
2024-04-26 11:31:10 +02:00

287 lines
7.5 KiB
Go

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())
var cerr *swaggererrors.CompositeError
if errors.As(validationErrors, &cerr) {
for _, desc := range cerr.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
}
if g.FlattenOpts.Expand {
// for a similar reason as the one mentioned above for validate,
// schema expansion alters the internal doc cache in the spec.
// This nasty bug (in spec expander) affects circular references.
// So we need to reload the spec from a clone.
// Notice that since the spec inside the document has been modified, we should
// ensure that Pristine refreshes its row root document.
specDoc = specDoc.Pristine()
}
// 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, 0o600); 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)
}