github.com/go-swagger/go-swagger@v0.31.0/generator/spec.go (about)

     1  package generator
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"log"
     8  	"os"
     9  	"path/filepath"
    10  
    11  	"github.com/go-openapi/analysis"
    12  	swaggererrors "github.com/go-openapi/errors"
    13  	"github.com/go-openapi/loads"
    14  	"github.com/go-openapi/spec"
    15  	"github.com/go-openapi/strfmt"
    16  	"github.com/go-openapi/swag"
    17  	"github.com/go-openapi/validate"
    18  
    19  	yamlv2 "gopkg.in/yaml.v2"
    20  )
    21  
    22  func (g *GenOpts) validateAndFlattenSpec() (*loads.Document, error) {
    23  	// Load spec document
    24  	specDoc, err := loads.Spec(g.Spec)
    25  	if err != nil {
    26  		return nil, err
    27  	}
    28  
    29  	// If accepts definitions only, add dummy swagger header to pass validation
    30  	if g.AcceptDefinitionsOnly {
    31  		specDoc, err = applyDefaultSwagger(specDoc)
    32  		if err != nil {
    33  			return nil, err
    34  		}
    35  	}
    36  
    37  	// Validate if needed
    38  	if g.ValidateSpec {
    39  		log.Printf("validating spec %v", g.Spec)
    40  		validationErrors := validate.Spec(specDoc, strfmt.Default)
    41  		if validationErrors != nil {
    42  			str := fmt.Sprintf("The swagger spec at %q is invalid against swagger specification %s. see errors :\n",
    43  				g.Spec, specDoc.Version())
    44  			var cerr *swaggererrors.CompositeError
    45  			if errors.As(validationErrors, &cerr) {
    46  				for _, desc := range cerr.Errors {
    47  					str += fmt.Sprintf("- %s\n", desc)
    48  				}
    49  			}
    50  			return nil, errors.New(str)
    51  		}
    52  		// TODO(fredbi): due to uncontrolled $ref state in spec, we need to reload the spec atm, or flatten won't
    53  		// work properly (validate expansion alters the $ref cache in go-openapi/spec)
    54  		specDoc, _ = loads.Spec(g.Spec)
    55  	}
    56  
    57  	// Flatten spec
    58  	//
    59  	// Some preprocessing is required before codegen
    60  	//
    61  	// This ensures at least that $ref's in the spec document are canonical,
    62  	// i.e all $ref are local to this file and point to some uniquely named definition.
    63  	//
    64  	// Default option is to ensure minimal flattening of $ref, bundling remote $refs and relocating arbitrary JSON
    65  	// pointers as definitions.
    66  	// This preprocessing may introduce duplicate names (e.g. remote $ref with same name). In this case, a definition
    67  	// suffixed with "OAIGen" is produced.
    68  	//
    69  	// Full flattening option farther transforms the spec by moving every complex object (e.g. with some properties)
    70  	// as a standalone definition.
    71  	//
    72  	// Eventually, an "expand spec" option is available. It is essentially useful for testing purposes.
    73  	//
    74  	// NOTE(fredbi): spec expansion may produce some unsupported constructs and is not yet protected against the
    75  	// following cases:
    76  	//  - polymorphic types generation may fail with expansion (expand destructs the reuse intent of the $ref in allOf)
    77  	//  - name duplicates may occur and result in compilation failures
    78  	//
    79  	// The right place to fix these shortcomings is go-openapi/analysis.
    80  
    81  	g.FlattenOpts.BasePath = specDoc.SpecFilePath()
    82  	g.FlattenOpts.Spec = analysis.New(specDoc.Spec())
    83  
    84  	g.printFlattenOpts()
    85  
    86  	if err = analysis.Flatten(*g.FlattenOpts); err != nil {
    87  		return nil, err
    88  	}
    89  
    90  	if g.FlattenOpts.Expand {
    91  		// for a similar reason as the one mentioned above for validate,
    92  		// schema expansion alters the internal doc cache in the spec.
    93  		// This nasty bug (in spec expander) affects circular references.
    94  		// So we need to reload the spec from a clone.
    95  		// Notice that since the spec inside the document has been modified, we should
    96  		// ensure that Pristine refreshes its row root document.
    97  		specDoc = specDoc.Pristine()
    98  	}
    99  
   100  	// yields the preprocessed spec document
   101  	return specDoc, nil
   102  }
   103  
   104  func (g *GenOpts) analyzeSpec() (*loads.Document, *analysis.Spec, error) {
   105  	// load, validate and flatten
   106  	specDoc, err := g.validateAndFlattenSpec()
   107  	if err != nil {
   108  		return nil, nil, err
   109  	}
   110  
   111  	// spec preprocessing option
   112  	if g.PropertiesSpecOrder {
   113  		g.Spec = WithAutoXOrder(g.Spec)
   114  		specDoc, err = loads.Spec(g.Spec)
   115  		if err != nil {
   116  			return nil, nil, err
   117  		}
   118  	}
   119  
   120  	// analyze the spec
   121  	analyzed := analysis.New(specDoc.Spec())
   122  
   123  	return specDoc, analyzed, nil
   124  }
   125  
   126  func (g *GenOpts) printFlattenOpts() {
   127  	var preprocessingOption string
   128  	switch {
   129  	case g.FlattenOpts.Expand:
   130  		preprocessingOption = "expand"
   131  	case g.FlattenOpts.Minimal:
   132  		preprocessingOption = "minimal flattening"
   133  	default:
   134  		preprocessingOption = "full flattening"
   135  	}
   136  	log.Printf("preprocessing spec with option:  %s", preprocessingOption)
   137  }
   138  
   139  // findSwaggerSpec fetches a default swagger spec if none is provided
   140  func findSwaggerSpec(nm string) (string, error) {
   141  	specs := []string{"swagger.json", "swagger.yml", "swagger.yaml"}
   142  	if nm != "" {
   143  		specs = []string{nm}
   144  	}
   145  	var name string
   146  	for _, nn := range specs {
   147  		f, err := os.Stat(nn)
   148  		if err != nil {
   149  			if os.IsNotExist(err) {
   150  				continue
   151  			}
   152  			return "", err
   153  		}
   154  		if f.IsDir() {
   155  			return "", fmt.Errorf("%s is a directory", nn)
   156  		}
   157  		name = nn
   158  		break
   159  	}
   160  	if name == "" {
   161  		return "", errors.New("couldn't find a swagger spec")
   162  	}
   163  	return name, nil
   164  }
   165  
   166  // WithAutoXOrder amends the spec to specify property order as they appear
   167  // in the spec (supports yaml documents only).
   168  func WithAutoXOrder(specPath string) string {
   169  	lookFor := func(ele interface{}, key string) (yamlv2.MapSlice, bool) {
   170  		if slice, ok := ele.(yamlv2.MapSlice); ok {
   171  			for _, v := range slice {
   172  				if v.Key == key {
   173  					if slice, ok := v.Value.(yamlv2.MapSlice); ok {
   174  						return slice, ok
   175  					}
   176  				}
   177  			}
   178  		}
   179  		return nil, false
   180  	}
   181  
   182  	var addXOrder func(interface{})
   183  	addXOrder = func(element interface{}) {
   184  		if props, ok := lookFor(element, "properties"); ok {
   185  			for i, prop := range props {
   186  				if pSlice, ok := prop.Value.(yamlv2.MapSlice); ok {
   187  					isObject := false
   188  					xOrderIndex := -1 // find if x-order already exists
   189  
   190  					for i, v := range pSlice {
   191  						if v.Key == "type" && v.Value == object {
   192  							isObject = true
   193  						}
   194  						if v.Key == xOrder {
   195  							xOrderIndex = i
   196  							break
   197  						}
   198  					}
   199  
   200  					if xOrderIndex > -1 { // override existing x-order
   201  						pSlice[xOrderIndex] = yamlv2.MapItem{Key: xOrder, Value: i}
   202  					} else { // append new x-order
   203  						pSlice = append(pSlice, yamlv2.MapItem{Key: xOrder, Value: i})
   204  					}
   205  					prop.Value = pSlice
   206  					props[i] = prop
   207  
   208  					if isObject {
   209  						addXOrder(pSlice)
   210  					}
   211  				}
   212  			}
   213  		}
   214  	}
   215  
   216  	data, err := swag.LoadFromFileOrHTTP(specPath)
   217  	if err != nil {
   218  		panic(err)
   219  	}
   220  
   221  	yamlDoc, err := BytesToYAMLv2Doc(data)
   222  	if err != nil {
   223  		panic(err)
   224  	}
   225  
   226  	if defs, ok := lookFor(yamlDoc, "definitions"); ok {
   227  		for _, def := range defs {
   228  			addXOrder(def.Value)
   229  		}
   230  	}
   231  
   232  	addXOrder(yamlDoc)
   233  
   234  	out, err := yamlv2.Marshal(yamlDoc)
   235  	if err != nil {
   236  		panic(err)
   237  	}
   238  
   239  	tmpDir, err := os.MkdirTemp("", "go-swagger-")
   240  	if err != nil {
   241  		panic(err)
   242  	}
   243  
   244  	tmpFile := filepath.Join(tmpDir, filepath.Base(specPath))
   245  	if err := os.WriteFile(tmpFile, out, 0o600); err != nil {
   246  		panic(err)
   247  	}
   248  	return tmpFile
   249  }
   250  
   251  // BytesToYAMLDoc converts a byte slice into a YAML document
   252  func BytesToYAMLv2Doc(data []byte) (interface{}, error) {
   253  	var canary map[interface{}]interface{} // validate this is an object and not a different type
   254  	if err := yamlv2.Unmarshal(data, &canary); err != nil {
   255  		return nil, err
   256  	}
   257  
   258  	var document yamlv2.MapSlice // preserve order that is present in the document
   259  	if err := yamlv2.Unmarshal(data, &document); err != nil {
   260  		return nil, err
   261  	}
   262  	return document, nil
   263  }
   264  
   265  func applyDefaultSwagger(doc *loads.Document) (*loads.Document, error) {
   266  	// bake a minimal swagger spec to pass validation
   267  	swspec := doc.Spec()
   268  	if swspec.Swagger == "" {
   269  		swspec.Swagger = "2.0"
   270  	}
   271  	if swspec.Info == nil {
   272  		info := new(spec.Info)
   273  		info.Version = "0.0.0"
   274  		info.Title = "minimal"
   275  		swspec.Info = info
   276  	}
   277  	if swspec.Paths == nil {
   278  		swspec.Paths = &spec.Paths{}
   279  	}
   280  	// rewrite the document with the new addition
   281  	jazon, err := json.Marshal(swspec)
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  	return loads.Analyzed(jazon, swspec.Swagger)
   286  }