mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-26 13:46:39 +00:00
fd8a724e77
* bump go swagger version * bump swagger version
287 lines
7.5 KiB
Go
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)
|
|
}
|