github.com/AngusLu/go-swagger@v0.28.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 // load, validate and flatten 93 specDoc, err := g.validateAndFlattenSpec() 94 if err != nil { 95 return nil, nil, err 96 } 97 98 // spec preprocessing option 99 if g.PropertiesSpecOrder { 100 g.Spec = WithAutoXOrder(g.Spec) 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 tmpDir, err := ioutil.TempDir("", "go-swagger-") 218 if err != nil { 219 panic(err) 220 } 221 222 tmpFile := filepath.Join(tmpDir, filepath.Base(specPath)) 223 if err := ioutil.WriteFile(tmpFile, out, 0600); err != nil { 224 panic(err) 225 } 226 return tmpFile 227 } 228 229 func applyDefaultSwagger(doc *loads.Document) (*loads.Document, error) { 230 // bake a minimal swagger spec to pass validation 231 swspec := doc.Spec() 232 if swspec.Swagger == "" { 233 swspec.Swagger = "2.0" 234 } 235 if swspec.Info == nil { 236 info := new(spec.Info) 237 info.Version = "0.0.0" 238 info.Title = "minimal" 239 swspec.Info = info 240 } 241 if swspec.Paths == nil { 242 swspec.Paths = &spec.Paths{} 243 } 244 // rewrite the document with the new addition 245 jazon, err := json.Marshal(swspec) 246 if err != nil { 247 return nil, err 248 } 249 return loads.Analyzed(jazon, swspec.Swagger) 250 }