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  }