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  }