github.com/josephspurrier/go-swagger@v0.2.1-0.20221129144919-1f672a142a00/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 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) (yamlv2.MapSlice, bool) { 153 if slice, ok := ele.(yamlv2.MapSlice); ok { 154 for _, v := range slice { 155 if v.Key == key { 156 if slice, ok := v.Value.(yamlv2.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.(yamlv2.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] = yamlv2.MapItem{Key: xOrder, Value: i} 185 } else { // append new x-order 186 pSlice = append(pSlice, yamlv2.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 data, err := swag.LoadFromFileOrHTTP(specPath) 200 if err != nil { 201 panic(err) 202 } 203 204 yamlDoc, err := BytesToYAMLv2Doc(data) 205 if err != nil { 206 panic(err) 207 } 208 209 if defs, ok := lookFor(yamlDoc, "definitions"); ok { 210 for _, def := range defs { 211 addXOrder(def.Value) 212 } 213 } 214 215 addXOrder(yamlDoc) 216 217 out, err := yamlv2.Marshal(yamlDoc) 218 if err != nil { 219 panic(err) 220 } 221 222 tmpDir, err := os.MkdirTemp("", "go-swagger-") 223 if err != nil { 224 panic(err) 225 } 226 227 tmpFile := filepath.Join(tmpDir, filepath.Base(specPath)) 228 if err := os.WriteFile(tmpFile, out, 0600); err != nil { 229 panic(err) 230 } 231 return tmpFile 232 } 233 234 // BytesToYAMLDoc converts a byte slice into a YAML document 235 func BytesToYAMLv2Doc(data []byte) (interface{}, error) { 236 var canary map[interface{}]interface{} // validate this is an object and not a different type 237 if err := yamlv2.Unmarshal(data, &canary); err != nil { 238 return nil, err 239 } 240 241 var document yamlv2.MapSlice // preserve order that is present in the document 242 if err := yamlv2.Unmarshal(data, &document); err != nil { 243 return nil, err 244 } 245 return document, nil 246 } 247 248 func applyDefaultSwagger(doc *loads.Document) (*loads.Document, error) { 249 // bake a minimal swagger spec to pass validation 250 swspec := doc.Spec() 251 if swspec.Swagger == "" { 252 swspec.Swagger = "2.0" 253 } 254 if swspec.Info == nil { 255 info := new(spec.Info) 256 info.Version = "0.0.0" 257 info.Title = "minimal" 258 swspec.Info = info 259 } 260 if swspec.Paths == nil { 261 swspec.Paths = &spec.Paths{} 262 } 263 // rewrite the document with the new addition 264 jazon, err := json.Marshal(swspec) 265 if err != nil { 266 return nil, err 267 } 268 return loads.Analyzed(jazon, swspec.Swagger) 269 }