github.com/rzurga/go-swagger@v0.28.1-0.20211109195225-5d1f453ffa3a/generator/language.go (about) 1 package generator 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io/ioutil" 7 "log" 8 "os" 9 "path" 10 "path/filepath" 11 "regexp" 12 goruntime "runtime" 13 "sort" 14 "strings" 15 16 "github.com/go-openapi/swag" 17 "golang.org/x/tools/imports" 18 ) 19 20 var ( 21 // DefaultLanguageFunc defines the default generation language 22 DefaultLanguageFunc func() *LanguageOpts 23 24 moduleRe *regexp.Regexp 25 ) 26 27 func initLanguage() { 28 DefaultLanguageFunc = GoLangOpts 29 30 moduleRe = regexp.MustCompile(`module[ \t]+([^\s]+)`) 31 } 32 33 // LanguageOpts to describe a language to the code generator 34 type LanguageOpts struct { 35 ReservedWords []string 36 BaseImportFunc func(string) string `json:"-"` 37 ImportsFunc func(map[string]string) string `json:"-"` 38 ArrayInitializerFunc func(interface{}) (string, error) `json:"-"` 39 reservedWordsSet map[string]struct{} 40 initialized bool 41 formatFunc func(string, []byte) ([]byte, error) 42 fileNameFunc func(string) string // language specific source file naming rules 43 dirNameFunc func(string) string // language specific directory naming rules 44 } 45 46 // Init the language option 47 func (l *LanguageOpts) Init() { 48 if l.initialized { 49 return 50 } 51 l.initialized = true 52 l.reservedWordsSet = make(map[string]struct{}) 53 for _, rw := range l.ReservedWords { 54 l.reservedWordsSet[rw] = struct{}{} 55 } 56 } 57 58 // MangleName makes sure a reserved word gets a safe name 59 func (l *LanguageOpts) MangleName(name, suffix string) string { 60 if _, ok := l.reservedWordsSet[swag.ToFileName(name)]; !ok { 61 return name 62 } 63 return strings.Join([]string{name, suffix}, "_") 64 } 65 66 // MangleVarName makes sure a reserved word gets a safe name 67 func (l *LanguageOpts) MangleVarName(name string) string { 68 nm := swag.ToVarName(name) 69 if _, ok := l.reservedWordsSet[nm]; !ok { 70 return nm 71 } 72 return nm + "Var" 73 } 74 75 // MangleFileName makes sure a file name gets a safe name 76 func (l *LanguageOpts) MangleFileName(name string) string { 77 if l.fileNameFunc != nil { 78 return l.fileNameFunc(name) 79 } 80 return swag.ToFileName(name) 81 } 82 83 // ManglePackageName makes sure a package gets a safe name. 84 // In case of a file system path (e.g. name contains "/" or "\" on Windows), this return only the last element. 85 func (l *LanguageOpts) ManglePackageName(name, suffix string) string { 86 if name == "" { 87 return suffix 88 } 89 if l.dirNameFunc != nil { 90 name = l.dirNameFunc(name) 91 } 92 pth := filepath.ToSlash(filepath.Clean(name)) // preserve path 93 pkg := importAlias(pth) // drop path 94 return l.MangleName(swag.ToFileName(prefixForName(pkg)+pkg), suffix) 95 } 96 97 // ManglePackagePath makes sure a full package path gets a safe name. 98 // Only the last part of the path is altered. 99 func (l *LanguageOpts) ManglePackagePath(name string, suffix string) string { 100 if name == "" { 101 return suffix 102 } 103 target := filepath.ToSlash(filepath.Clean(name)) // preserve path 104 parts := strings.Split(target, "/") 105 parts[len(parts)-1] = l.ManglePackageName(parts[len(parts)-1], suffix) 106 return strings.Join(parts, "/") 107 } 108 109 // FormatContent formats a file with a language specific formatter 110 func (l *LanguageOpts) FormatContent(name string, content []byte) ([]byte, error) { 111 if l.formatFunc != nil { 112 return l.formatFunc(name, content) 113 } 114 return content, nil 115 } 116 117 // imports generate the code to import some external packages, possibly aliased 118 func (l *LanguageOpts) imports(imports map[string]string) string { 119 if l.ImportsFunc != nil { 120 return l.ImportsFunc(imports) 121 } 122 return "" 123 } 124 125 // arrayInitializer builds a litteral array 126 func (l *LanguageOpts) arrayInitializer(data interface{}) (string, error) { 127 if l.ArrayInitializerFunc != nil { 128 return l.ArrayInitializerFunc(data) 129 } 130 return "", nil 131 } 132 133 // baseImport figures out the base path to generate import statements 134 func (l *LanguageOpts) baseImport(tgt string) string { 135 if l.BaseImportFunc != nil { 136 return l.BaseImportFunc(tgt) 137 } 138 debugLog("base import func is nil") 139 return "" 140 } 141 142 // GoLangOpts for rendering items as golang code 143 func GoLangOpts() *LanguageOpts { 144 var goOtherReservedSuffixes = map[string]bool{ 145 // see: 146 // https://golang.org/src/go/build/syslist.go 147 // https://golang.org/doc/install/source#environment 148 149 // goos 150 "aix": true, 151 "android": true, 152 "darwin": true, 153 "dragonfly": true, 154 "freebsd": true, 155 "hurd": true, 156 "illumos": true, 157 "js": true, 158 "linux": true, 159 "nacl": true, 160 "netbsd": true, 161 "openbsd": true, 162 "plan9": true, 163 "solaris": true, 164 "windows": true, 165 "zos": true, 166 167 // arch 168 "386": true, 169 "amd64": true, 170 "amd64p32": true, 171 "arm": true, 172 "armbe": true, 173 "arm64": true, 174 "arm64be": true, 175 "mips": true, 176 "mipsle": true, 177 "mips64": true, 178 "mips64le": true, 179 "mips64p32": true, 180 "mips64p32le": true, 181 "ppc": true, 182 "ppc64": true, 183 "ppc64le": true, 184 "riscv": true, 185 "riscv64": true, 186 "s390": true, 187 "s390x": true, 188 "sparc": true, 189 "sparc64": true, 190 "wasm": true, 191 192 // other reserved suffixes 193 "test": true, 194 } 195 196 opts := new(LanguageOpts) 197 opts.ReservedWords = []string{ 198 "break", "default", "func", "interface", "select", 199 "case", "defer", "go", "map", "struct", 200 "chan", "else", "goto", "package", "switch", 201 "const", "fallthrough", "if", "range", "type", 202 "continue", "for", "import", "return", "var", 203 } 204 205 opts.formatFunc = func(ffn string, content []byte) ([]byte, error) { 206 opts := new(imports.Options) 207 opts.TabIndent = true 208 opts.TabWidth = 2 209 opts.Fragment = true 210 opts.Comments = true 211 return imports.Process(ffn, content, opts) 212 } 213 214 opts.fileNameFunc = func(name string) string { 215 // whenever a generated file name ends with a suffix 216 // that is meaningful to go build, adds a "swagger" 217 // suffix 218 parts := strings.Split(swag.ToFileName(name), "_") 219 if goOtherReservedSuffixes[parts[len(parts)-1]] { 220 // file name ending with a reserved arch or os name 221 // are appended an innocuous suffix "swagger" 222 parts = append(parts, "swagger") 223 } 224 return strings.Join(parts, "_") 225 } 226 227 opts.dirNameFunc = func(name string) string { 228 // whenever a generated directory name is a special 229 // golang directory, append an innocuous suffix 230 switch name { 231 case "vendor", "internal": 232 return strings.Join([]string{name, "swagger"}, "_") 233 } 234 return name 235 } 236 237 opts.ImportsFunc = func(imports map[string]string) string { 238 if len(imports) == 0 { 239 return "" 240 } 241 result := make([]string, 0, len(imports)) 242 for k, v := range imports { 243 _, name := path.Split(v) 244 if name != k { 245 result = append(result, fmt.Sprintf("\t%s %q", k, v)) 246 } else { 247 result = append(result, fmt.Sprintf("\t%q", v)) 248 } 249 } 250 sort.Strings(result) 251 return strings.Join(result, "\n") 252 } 253 254 opts.ArrayInitializerFunc = func(data interface{}) (string, error) { 255 // ArrayInitializer constructs a Go literal initializer from interface{} literals. 256 // e.g. []interface{}{"a", "b"} is transformed in {"a","b",} 257 // e.g. map[string]interface{}{ "a": "x", "b": "y"} is transformed in {"a":"x","b":"y",}. 258 // 259 // NOTE: this is currently used to construct simple slice intializers for default values. 260 // This allows for nicer slice initializers for slices of primitive types and avoid systematic use for json.Unmarshal(). 261 b, err := json.Marshal(data) 262 if err != nil { 263 return "", err 264 } 265 return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(string(b), "}", ",}"), "[", "{"), "]", ",}"), "{,}", "{}"), nil 266 } 267 268 opts.BaseImportFunc = func(tgt string) string { 269 tgt = filepath.Clean(tgt) 270 // On Windows, filepath.Abs("") behaves differently than on Unix. 271 // Windows: yields an error, since Abs() does not know the volume. 272 // UNIX: returns current working directory 273 if tgt == "" { 274 tgt = "." 275 } 276 tgtAbsPath, err := filepath.Abs(tgt) 277 if err != nil { 278 log.Fatalf("could not evaluate base import path with target \"%s\": %v", tgt, err) 279 } 280 281 var tgtAbsPathExtended string 282 tgtAbsPathExtended, err = filepath.EvalSymlinks(tgtAbsPath) 283 if err != nil { 284 log.Fatalf("could not evaluate base import path with target \"%s\" (with symlink resolution): %v", tgtAbsPath, err) 285 } 286 287 gopath := os.Getenv("GOPATH") 288 if gopath == "" { 289 gopath = filepath.Join(os.Getenv("HOME"), "go") 290 } 291 292 var pth string 293 for _, gp := range filepath.SplitList(gopath) { 294 // EvalSymLinks also calls the Clean 295 gopathExtended, er := filepath.EvalSymlinks(gp) 296 if er != nil { 297 log.Fatalln(er) 298 } 299 gopathExtended = filepath.Join(gopathExtended, "src") 300 gp = filepath.Join(gp, "src") 301 302 // At this stage we have expanded and unexpanded target path. GOPATH is fully expanded. 303 // Expanded means symlink free. 304 // We compare both types of targetpath<s> with gopath. 305 // If any one of them coincides with gopath , it is imperative that 306 // target path lies inside gopath. How? 307 // - Case 1: Irrespective of symlinks paths coincide. Both non-expanded paths. 308 // - Case 2: Symlink in target path points to location inside GOPATH. (Expanded Target Path) 309 // - Case 3: Symlink in target path points to directory outside GOPATH (Unexpanded target path) 310 311 // Case 1: - Do nothing case. If non-expanded paths match just generate base import path as if 312 // there are no symlinks. 313 314 // Case 2: - Symlink in target path points to location inside GOPATH. (Expanded Target Path) 315 // First if will fail. Second if will succeed. 316 317 // Case 3: - Symlink in target path points to directory outside GOPATH (Unexpanded target path) 318 // First if will succeed and break. 319 320 // compares non expanded path for both 321 if ok, relativepath := checkPrefixAndFetchRelativePath(tgtAbsPath, gp); ok { 322 pth = relativepath 323 break 324 } 325 326 // Compares non-expanded target path 327 if ok, relativepath := checkPrefixAndFetchRelativePath(tgtAbsPath, gopathExtended); ok { 328 pth = relativepath 329 break 330 } 331 332 // Compares expanded target path. 333 if ok, relativepath := checkPrefixAndFetchRelativePath(tgtAbsPathExtended, gopathExtended); ok { 334 pth = relativepath 335 break 336 } 337 338 } 339 340 mod, goModuleAbsPath, err := tryResolveModule(tgtAbsPath) 341 switch { 342 case err != nil: 343 log.Fatalf("Failed to resolve module using go.mod file: %s", err) 344 case mod != "": 345 relTgt := relPathToRelGoPath(goModuleAbsPath, tgtAbsPath) 346 if !strings.HasSuffix(mod, relTgt) { 347 return filepath.ToSlash(mod + relTgt) 348 } 349 return filepath.ToSlash(mod) 350 } 351 352 if pth == "" { 353 log.Fatalln("target must reside inside a location in the $GOPATH/src or be a module") 354 } 355 return filepath.ToSlash(pth) 356 } 357 opts.Init() 358 return opts 359 } 360 361 // resolveGoModFile walks up the directory tree starting from 'dir' until it 362 // finds a go.mod file. If go.mod is found it will return the related file 363 // object. If no go.mod file is found it will return an error. 364 func resolveGoModFile(dir string) (*os.File, string, error) { 365 goModPath := filepath.Join(dir, "go.mod") 366 f, err := os.Open(goModPath) 367 if err != nil { 368 if os.IsNotExist(err) && dir != filepath.Dir(dir) { 369 return resolveGoModFile(filepath.Dir(dir)) 370 } 371 return nil, "", err 372 } 373 return f, dir, nil 374 } 375 376 // relPathToRelGoPath takes a relative os path and returns the relative go 377 // package path. For unix nothing will change but for windows \ will be 378 // converted to /. 379 func relPathToRelGoPath(modAbsPath, absPath string) string { 380 if absPath == "." { 381 return "" 382 } 383 384 path := strings.TrimPrefix(absPath, modAbsPath) 385 pathItems := strings.Split(path, string(filepath.Separator)) 386 return strings.Join(pathItems, "/") 387 } 388 389 func tryResolveModule(baseTargetPath string) (string, string, error) { 390 f, goModAbsPath, err := resolveGoModFile(baseTargetPath) 391 switch { 392 case os.IsNotExist(err): 393 return "", "", nil 394 case err != nil: 395 return "", "", err 396 } 397 398 src, err := ioutil.ReadAll(f) 399 if err != nil { 400 return "", "", err 401 } 402 403 match := moduleRe.FindSubmatch(src) 404 if len(match) != 2 { 405 return "", "", nil 406 } 407 408 return string(match[1]), goModAbsPath, nil 409 } 410 411 // 1. Checks if the child path and parent path coincide. 412 // 2. If they do return child path relative to parent path. 413 // 3. Everything else return false 414 func checkPrefixAndFetchRelativePath(childpath string, parentpath string) (bool, string) { 415 // Windows (local) file systems - NTFS, as well as FAT and variants 416 // are case insensitive. 417 cp, pp := childpath, parentpath 418 if goruntime.GOOS == "windows" { 419 cp = strings.ToLower(cp) 420 pp = strings.ToLower(pp) 421 } 422 423 if strings.HasPrefix(cp, pp) { 424 pth, err := filepath.Rel(parentpath, childpath) 425 if err != nil { 426 log.Fatalln(err) 427 } 428 return true, pth 429 } 430 431 return false, "" 432 433 }