github.com/6543-forks/go-swagger@v0.26.0/generator/spec.go (about)

     1  package generator
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"log"
     9  	"os"
    10  	"path/filepath"
    11  
    12  	"github.com/go-openapi/analysis"
    13  	swaggererrors "github.com/go-openapi/errors"
    14  	"github.com/go-openapi/loads"
    15  	"github.com/go-openapi/spec"
    16  	"github.com/go-openapi/strfmt"
    17  	"github.com/go-openapi/swag"
    18  	"github.com/go-openapi/validate"
    19  	"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  			for _, desc := range validationErrors.(*swaggererrors.CompositeError).Errors {
    45  				str += fmt.Sprintf("- %s\n", desc)
    46  			}
    47  			return nil, errors.New(str)
    48  		}
    49  		// TODO(fredbi): due to uncontrolled $ref state in spec, we need to reload the spec atm, or flatten won't
    50  		// work properly (validate expansion alters the $ref cache in go-openapi/spec)
    51  		specDoc, _ = loads.Spec(g.Spec)
    52  	}
    53  
    54  	// Flatten spec
    55  	//
    56  	// Some preprocessing is required before codegen
    57  	//
    58  	// This ensures at least that $ref's in the spec document are canonical,
    59  	// i.e all $ref are local to this file and point to some uniquely named definition.
    60  	//
    61  	// Default option is to ensure minimal flattening of $ref, bundling remote $refs and relocating arbitrary JSON
    62  	// pointers as definitions.
    63  	// This preprocessing may introduce duplicate names (e.g. remote $ref with same name). In this case, a definition
    64  	// suffixed with "OAIGen" is produced.
    65  	//
    66  	// Full flattening option farther transforms the spec by moving every complex object (e.g. with some properties)
    67  	// as a standalone definition.
    68  	//
    69  	// Eventually, an "expand spec" option is available. It is essentially useful for testing purposes.
    70  	//
    71  	// NOTE(fredbi): spec expansion may produce some unsupported constructs and is not yet protected against the
    72  	// following cases:
    73  	//  - polymorphic types generation may fail with expansion (expand destructs the reuse intent of the $ref in allOf)
    74  	//  - name duplicates may occur and result in compilation failures
    75  	//
    76  	// The right place to fix these shortcomings is go-openapi/analysis.
    77  
    78  	g.FlattenOpts.BasePath = specDoc.SpecFilePath()
    79  	g.FlattenOpts.Spec = analysis.New(specDoc.Spec())
    80  
    81  	g.printFlattenOpts()
    82  
    83  	if err = analysis.Flatten(*g.FlattenOpts); err != nil {
    84  		return nil, err
    85  	}
    86  
    87  	// yields the preprocessed spec document
    88  	return specDoc, nil
    89  }
    90  
    91  func (g *GenOpts) analyzeSpec() (*loads.Document, *analysis.Spec, error) {
    92  	// spec preprocessing option
    93  	if g.PropertiesSpecOrder {
    94  		g.Spec = WithAutoXOrder(g.Spec)
    95  	}
    96  
    97  	// load, validate and flatten
    98  	specDoc, err := g.validateAndFlattenSpec()
    99  	if err != nil {
   100  		return nil, nil, err
   101  	}
   102  
   103  	// analyze the spec
   104  	analyzed := analysis.New(specDoc.Spec())
   105  
   106  	return specDoc, analyzed, nil
   107  }
   108  
   109  func (g *GenOpts) printFlattenOpts() {
   110  	var preprocessingOption string
   111  	switch {
   112  	case g.FlattenOpts.Expand:
   113  		preprocessingOption = "expand"
   114  	case g.FlattenOpts.Minimal:
   115  		preprocessingOption = "minimal flattening"
   116  	default:
   117  		preprocessingOption = "full flattening"
   118  	}
   119  	log.Printf("preprocessing spec with option:  %s", preprocessingOption)
   120  }
   121  
   122  //findSwaggerSpec fetches a default swagger spec if none is provided
   123  func findSwaggerSpec(nm string) (string, error) {
   124  	specs := []string{"swagger.json", "swagger.yml", "swagger.yaml"}
   125  	if nm != "" {
   126  		specs = []string{nm}
   127  	}
   128  	var name string
   129  	for _, nn := range specs {
   130  		f, err := os.Stat(nn)
   131  		if err != nil {
   132  			if os.IsNotExist(err) {
   133  				continue
   134  			}
   135  			return "", err
   136  		}
   137  		if f.IsDir() {
   138  			return "", fmt.Errorf("%s is a directory", nn)
   139  		}
   140  		name = nn
   141  		break
   142  	}
   143  	if name == "" {
   144  		return "", errors.New("couldn't find a swagger spec")
   145  	}
   146  	return name, nil
   147  }
   148  
   149  // WithAutoXOrder amends the spec to specify property order as they appear
   150  // in the spec (supports yaml documents only).
   151  func WithAutoXOrder(specPath string) string {
   152  	lookFor := func(ele interface{}, key string) (yaml.MapSlice, bool) {
   153  		if slice, ok := ele.(yaml.MapSlice); ok {
   154  			for _, v := range slice {
   155  				if v.Key == key {
   156  					if slice, ok := v.Value.(yaml.MapSlice); ok {
   157  						return slice, ok
   158  					}
   159  				}
   160  			}
   161  		}
   162  		return nil, false
   163  	}
   164  
   165  	var addXOrder func(interface{})
   166  	addXOrder = func(element interface{}) {
   167  		if props, ok := lookFor(element, "properties"); ok {
   168  			for i, prop := range props {
   169  				if pSlice, ok := prop.Value.(yaml.MapSlice); ok {
   170  					isObject := false
   171  					xOrderIndex := -1 //Find if x-order already exists
   172  
   173  					for i, v := range pSlice {
   174  						if v.Key == "type" && v.Value == object {
   175  							isObject = true
   176  						}
   177  						if v.Key == xOrder {
   178  							xOrderIndex = i
   179  							break
   180  						}
   181  					}
   182  
   183  					if xOrderIndex > -1 { //Override existing x-order
   184  						pSlice[xOrderIndex] = yaml.MapItem{Key: xOrder, Value: i}
   185  					} else { // append new x-order
   186  						pSlice = append(pSlice, yaml.MapItem{Key: xOrder, Value: i})
   187  					}
   188  					prop.Value = pSlice
   189  					props[i] = prop
   190  
   191  					if isObject {
   192  						addXOrder(pSlice)
   193  					}
   194  				}
   195  			}
   196  		}
   197  	}
   198  
   199  	yamlDoc, err := swag.YAMLData(specPath)
   200  	if err != nil {
   201  		panic(err)
   202  	}
   203  
   204  	if defs, ok := lookFor(yamlDoc, "definitions"); ok {
   205  		for _, def := range defs {
   206  			addXOrder(def.Value)
   207  		}
   208  	}
   209  
   210  	addXOrder(yamlDoc)
   211  
   212  	out, err := yaml.Marshal(yamlDoc)
   213  	if err != nil {
   214  		panic(err)
   215  	}
   216  
   217  	tmpFile, err := ioutil.TempFile("", filepath.Base(specPath))
   218  	if err != nil {
   219  		panic(err)
   220  	}
   221  	if err := ioutil.WriteFile(tmpFile.Name(), out, 0); err != nil {
   222  		panic(err)
   223  	}
   224  	return tmpFile.Name()
   225  }
   226  
   227  func applyDefaultSwagger(doc *loads.Document) (*loads.Document, error) {
   228  	// bake a minimal swagger spec to pass validation
   229  	swspec := doc.Spec()
   230  	if swspec.Swagger == "" {
   231  		swspec.Swagger = "2.0"
   232  	}
   233  	if swspec.Info == nil {
   234  		info := new(spec.Info)
   235  		info.Version = "0.0.0"
   236  		info.Title = "minimal"
   237  		swspec.Info = info
   238  	}
   239  	if swspec.Paths == nil {
   240  		swspec.Paths = &spec.Paths{}
   241  	}
   242  	// rewrite the document with the new addition
   243  	jazon, err := json.Marshal(swspec)
   244  	if err != nil {
   245  		return nil, err
   246  	}
   247  	return loads.Analyzed(jazon, swspec.Swagger)
   248  }