github.com/thetreep/go-swagger@v0.0.0-20240223100711-35af64f14f01/generator/language.go (about) 1 package generator 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 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 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 "ios": true, 158 "js": true, 159 "linux": true, 160 "nacl": true, 161 "netbsd": true, 162 "openbsd": true, 163 "plan9": true, 164 "solaris": true, 165 "windows": true, 166 "zos": true, 167 168 // arch 169 "386": true, 170 "amd64": true, 171 "amd64p32": true, 172 "arm": true, 173 "armbe": true, 174 "arm64": true, 175 "arm64be": true, 176 "loong64": true, 177 "mips": true, 178 "mipsle": true, 179 "mips64": true, 180 "mips64le": true, 181 "mips64p32": true, 182 "mips64p32le": true, 183 "ppc": true, 184 "ppc64": true, 185 "ppc64le": true, 186 "riscv": true, 187 "riscv64": true, 188 "s390": true, 189 "s390x": true, 190 "sparc": true, 191 "sparc64": true, 192 "wasm": true, 193 194 // other reserved suffixes 195 "test": true, 196 } 197 198 opts := new(LanguageOpts) 199 opts.ReservedWords = []string{ 200 "break", "default", "func", "interface", "select", 201 "case", "defer", "go", "map", "struct", 202 "chan", "else", "goto", "package", "switch", 203 "const", "fallthrough", "if", "range", "type", 204 "continue", "for", "import", "return", "var", 205 } 206 207 opts.formatFunc = func(ffn string, content []byte) ([]byte, error) { 208 opts := new(imports.Options) 209 opts.TabIndent = true 210 opts.TabWidth = 2 211 opts.Fragment = true 212 opts.Comments = true 213 return imports.Process(ffn, content, opts) 214 } 215 216 opts.fileNameFunc = func(name string) string { 217 // whenever a generated file name ends with a suffix 218 // that is meaningful to go build, adds a "swagger" 219 // suffix 220 parts := strings.Split(swag.ToFileName(name), "_") 221 if goOtherReservedSuffixes[parts[len(parts)-1]] { 222 // file name ending with a reserved arch or os name 223 // are appended an innocuous suffix "swagger" 224 parts = append(parts, "swagger") 225 } 226 return strings.Join(parts, "_") 227 } 228 229 opts.dirNameFunc = func(name string) string { 230 // whenever a generated directory name is a special 231 // golang directory, append an innocuous suffix 232 switch name { 233 case "vendor", "internal": 234 return strings.Join([]string{name, "swagger"}, "_") 235 } 236 return name 237 } 238 239 opts.ImportsFunc = func(imports map[string]string) string { 240 if len(imports) == 0 { 241 return "" 242 } 243 result := make([]string, 0, len(imports)) 244 for k, v := range imports { 245 _, name := path.Split(v) 246 if name != k { 247 result = append(result, fmt.Sprintf("\t%s %q", k, v)) 248 } else { 249 result = append(result, fmt.Sprintf("\t%q", v)) 250 } 251 } 252 sort.Strings(result) 253 return strings.Join(result, "\n") 254 } 255 256 opts.ArrayInitializerFunc = func(data interface{}) (string, error) { 257 // ArrayInitializer constructs a Go literal initializer from interface{} literals. 258 // e.g. []interface{}{"a", "b"} is transformed in {"a","b",} 259 // e.g. map[string]interface{}{ "a": "x", "b": "y"} is transformed in {"a":"x","b":"y",}. 260 // 261 // NOTE: this is currently used to construct simple slice intializers for default values. 262 // This allows for nicer slice initializers for slices of primitive types and avoid systematic use for json.Unmarshal(). 263 b, err := json.Marshal(data) 264 if err != nil { 265 return "", err 266 } 267 return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(string(b), "}", ",}"), "[", "{"), "]", ",}"), "{,}", "{}"), nil 268 } 269 270 opts.BaseImportFunc = func(tgt string) string { 271 tgt = filepath.Clean(tgt) 272 // On Windows, filepath.Abs("") behaves differently than on Unix. 273 // Windows: yields an error, since Abs() does not know the volume. 274 // UNIX: returns current working directory 275 if tgt == "" { 276 tgt = "." 277 } 278 tgtAbsPath, err := filepath.Abs(tgt) 279 if err != nil { 280 log.Fatalf("could not evaluate base import path with target \"%s\": %v", tgt, err) 281 } 282 283 var tgtAbsPathExtended string 284 tgtAbsPathExtended, err = filepath.EvalSymlinks(tgtAbsPath) 285 if err != nil { 286 log.Fatalf("could not evaluate base import path with target \"%s\" (with symlink resolution): %v", tgtAbsPath, err) 287 } 288 289 gopath := os.Getenv("GOPATH") 290 if gopath == "" { 291 homeDir, herr := os.UserHomeDir() 292 if herr != nil { 293 log.Fatalln(herr) 294 } 295 gopath = filepath.Join(homeDir, "go") 296 } 297 298 var pth string 299 for _, gp := range filepath.SplitList(gopath) { 300 if _, derr := os.Stat(filepath.Join(gp, "src")); os.IsNotExist(derr) { 301 continue 302 } 303 // EvalSymLinks also calls the Clean 304 gopathExtended, er := filepath.EvalSymlinks(gp) 305 if er != nil { 306 panic(er) 307 } 308 gopathExtended = filepath.Join(gopathExtended, "src") 309 gp = filepath.Join(gp, "src") 310 311 // At this stage we have expanded and unexpanded target path. GOPATH is fully expanded. 312 // Expanded means symlink free. 313 // We compare both types of targetpath<s> with gopath. 314 // If any one of them coincides with gopath , it is imperative that 315 // target path lies inside gopath. How? 316 // - Case 1: Irrespective of symlinks paths coincide. Both non-expanded paths. 317 // - Case 2: Symlink in target path points to location inside GOPATH. (Expanded Target Path) 318 // - Case 3: Symlink in target path points to directory outside GOPATH (Unexpanded target path) 319 320 // Case 1: - Do nothing case. If non-expanded paths match just generate base import path as if 321 // there are no symlinks. 322 323 // Case 2: - Symlink in target path points to location inside GOPATH. (Expanded Target Path) 324 // First if will fail. Second if will succeed. 325 326 // Case 3: - Symlink in target path points to directory outside GOPATH (Unexpanded target path) 327 // First if will succeed and break. 328 329 // compares non expanded path for both 330 if ok, relativepath := checkPrefixAndFetchRelativePath(tgtAbsPath, gp); ok { 331 pth = relativepath 332 break 333 } 334 335 // Compares non-expanded target path 336 if ok, relativepath := checkPrefixAndFetchRelativePath(tgtAbsPath, gopathExtended); ok { 337 pth = relativepath 338 break 339 } 340 341 // Compares expanded target path. 342 if ok, relativepath := checkPrefixAndFetchRelativePath(tgtAbsPathExtended, gopathExtended); ok { 343 pth = relativepath 344 break 345 } 346 347 } 348 349 mod, goModuleAbsPath, err := tryResolveModule(tgtAbsPath) 350 switch { 351 case err != nil: 352 log.Fatalf("Failed to resolve module using go.mod file: %s", err) 353 case mod != "": 354 relTgt := relPathToRelGoPath(goModuleAbsPath, tgtAbsPath) 355 if !strings.HasSuffix(mod, relTgt) { 356 return filepath.ToSlash(mod + relTgt) 357 } 358 return filepath.ToSlash(mod) 359 } 360 361 if pth == "" { 362 log.Fatalln("target must reside inside a location in the $GOPATH/src or be a module") 363 } 364 return filepath.ToSlash(pth) 365 } 366 opts.Init() 367 return opts 368 } 369 370 // resolveGoModFile walks up the directory tree starting from 'dir' until it 371 // finds a go.mod file. If go.mod is found it will return the related file 372 // object. If no go.mod file is found it will return an error. 373 func resolveGoModFile(dir string) (*os.File, string, error) { 374 goModPath := filepath.Join(dir, "go.mod") 375 f, err := os.Open(goModPath) 376 if err != nil { 377 if os.IsNotExist(err) && dir != filepath.Dir(dir) { 378 return resolveGoModFile(filepath.Dir(dir)) 379 } 380 return nil, "", err 381 } 382 return f, dir, nil 383 } 384 385 // relPathToRelGoPath takes a relative os path and returns the relative go 386 // package path. For unix nothing will change but for windows \ will be 387 // converted to /. 388 func relPathToRelGoPath(modAbsPath, absPath string) string { 389 if absPath == "." { 390 return "" 391 } 392 393 path := strings.TrimPrefix(absPath, modAbsPath) 394 pathItems := strings.Split(path, string(filepath.Separator)) 395 return strings.Join(pathItems, "/") 396 } 397 398 func tryResolveModule(baseTargetPath string) (string, string, error) { 399 f, goModAbsPath, err := resolveGoModFile(baseTargetPath) 400 switch { 401 case os.IsNotExist(err): 402 return "", "", nil 403 case err != nil: 404 return "", "", err 405 } 406 407 src, err := io.ReadAll(f) 408 if err != nil { 409 return "", "", err 410 } 411 412 match := moduleRe.FindSubmatch(src) 413 if len(match) != 2 { 414 return "", "", nil 415 } 416 417 return string(match[1]), goModAbsPath, nil 418 } 419 420 // 1. Checks if the child path and parent path coincide. 421 // 2. If they do return child path relative to parent path. 422 // 3. Everything else return false 423 func checkPrefixAndFetchRelativePath(childpath string, parentpath string) (bool, string) { 424 // Windows (local) file systems - NTFS, as well as FAT and variants 425 // are case insensitive. 426 cp, pp := childpath, parentpath 427 if goruntime.GOOS == "windows" { 428 cp = strings.ToLower(cp) 429 pp = strings.ToLower(pp) 430 } 431 432 if strings.HasPrefix(cp, pp) { 433 pth, err := filepath.Rel(parentpath, childpath) 434 if err != nil { 435 log.Fatalln(err) 436 } 437 return true, pth 438 } 439 440 return false, "" 441 }