github.com/josephspurrier/go-swagger@v0.2.1-0.20221129144919-1f672a142a00/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 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 homeDir, herr := os.UserHomeDir() 290 if herr != nil { 291 log.Fatalln(herr) 292 } 293 gopath = filepath.Join(homeDir, "go") 294 } 295 296 var pth string 297 for _, gp := range filepath.SplitList(gopath) { 298 if _, derr := os.Stat(filepath.Join(gp, "src")); os.IsNotExist(derr) { 299 continue 300 } 301 // EvalSymLinks also calls the Clean 302 gopathExtended, er := filepath.EvalSymlinks(gp) 303 if er != nil { 304 panic(er) 305 } 306 gopathExtended = filepath.Join(gopathExtended, "src") 307 gp = filepath.Join(gp, "src") 308 309 // At this stage we have expanded and unexpanded target path. GOPATH is fully expanded. 310 // Expanded means symlink free. 311 // We compare both types of targetpath<s> with gopath. 312 // If any one of them coincides with gopath , it is imperative that 313 // target path lies inside gopath. How? 314 // - Case 1: Irrespective of symlinks paths coincide. Both non-expanded paths. 315 // - Case 2: Symlink in target path points to location inside GOPATH. (Expanded Target Path) 316 // - Case 3: Symlink in target path points to directory outside GOPATH (Unexpanded target path) 317 318 // Case 1: - Do nothing case. If non-expanded paths match just generate base import path as if 319 // there are no symlinks. 320 321 // Case 2: - Symlink in target path points to location inside GOPATH. (Expanded Target Path) 322 // First if will fail. Second if will succeed. 323 324 // Case 3: - Symlink in target path points to directory outside GOPATH (Unexpanded target path) 325 // First if will succeed and break. 326 327 // compares non expanded path for both 328 if ok, relativepath := checkPrefixAndFetchRelativePath(tgtAbsPath, gp); ok { 329 pth = relativepath 330 break 331 } 332 333 // Compares non-expanded target path 334 if ok, relativepath := checkPrefixAndFetchRelativePath(tgtAbsPath, gopathExtended); ok { 335 pth = relativepath 336 break 337 } 338 339 // Compares expanded target path. 340 if ok, relativepath := checkPrefixAndFetchRelativePath(tgtAbsPathExtended, gopathExtended); ok { 341 pth = relativepath 342 break 343 } 344 345 } 346 347 mod, goModuleAbsPath, err := tryResolveModule(tgtAbsPath) 348 switch { 349 case err != nil: 350 log.Fatalf("Failed to resolve module using go.mod file: %s", err) 351 case mod != "": 352 relTgt := relPathToRelGoPath(goModuleAbsPath, tgtAbsPath) 353 if !strings.HasSuffix(mod, relTgt) { 354 return filepath.ToSlash(mod + relTgt) 355 } 356 return filepath.ToSlash(mod) 357 } 358 359 if pth == "" { 360 log.Fatalln("target must reside inside a location in the $GOPATH/src or be a module") 361 } 362 return filepath.ToSlash(pth) 363 } 364 opts.Init() 365 return opts 366 } 367 368 // resolveGoModFile walks up the directory tree starting from 'dir' until it 369 // finds a go.mod file. If go.mod is found it will return the related file 370 // object. If no go.mod file is found it will return an error. 371 func resolveGoModFile(dir string) (*os.File, string, error) { 372 goModPath := filepath.Join(dir, "go.mod") 373 f, err := os.Open(goModPath) 374 if err != nil { 375 if os.IsNotExist(err) && dir != filepath.Dir(dir) { 376 return resolveGoModFile(filepath.Dir(dir)) 377 } 378 return nil, "", err 379 } 380 return f, dir, nil 381 } 382 383 // relPathToRelGoPath takes a relative os path and returns the relative go 384 // package path. For unix nothing will change but for windows \ will be 385 // converted to /. 386 func relPathToRelGoPath(modAbsPath, absPath string) string { 387 if absPath == "." { 388 return "" 389 } 390 391 path := strings.TrimPrefix(absPath, modAbsPath) 392 pathItems := strings.Split(path, string(filepath.Separator)) 393 return strings.Join(pathItems, "/") 394 } 395 396 func tryResolveModule(baseTargetPath string) (string, string, error) { 397 f, goModAbsPath, err := resolveGoModFile(baseTargetPath) 398 switch { 399 case os.IsNotExist(err): 400 return "", "", nil 401 case err != nil: 402 return "", "", err 403 } 404 405 src, err := io.ReadAll(f) 406 if err != nil { 407 return "", "", err 408 } 409 410 match := moduleRe.FindSubmatch(src) 411 if len(match) != 2 { 412 return "", "", nil 413 } 414 415 return string(match[1]), goModAbsPath, nil 416 } 417 418 // 1. Checks if the child path and parent path coincide. 419 // 2. If they do return child path relative to parent path. 420 // 3. Everything else return false 421 func checkPrefixAndFetchRelativePath(childpath string, parentpath string) (bool, string) { 422 // Windows (local) file systems - NTFS, as well as FAT and variants 423 // are case insensitive. 424 cp, pp := childpath, parentpath 425 if goruntime.GOOS == "windows" { 426 cp = strings.ToLower(cp) 427 pp = strings.ToLower(pp) 428 } 429 430 if strings.HasPrefix(cp, pp) { 431 pth, err := filepath.Rel(parentpath, childpath) 432 if err != nil { 433 log.Fatalln(err) 434 } 435 return true, pth 436 } 437 438 return false, "" 439 440 }