github.com/thetreep/go-swagger@v0.0.0-20240223100711-35af64f14f01/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 }