github.com/go-swagger/go-swagger@v0.31.0/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  }