forked from gitea/gitea
434 lines
12 KiB
Go
434 lines
12 KiB
Go
package generator
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
goruntime "runtime"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/go-openapi/swag"
|
|
"golang.org/x/tools/imports"
|
|
)
|
|
|
|
var (
|
|
// DefaultLanguageFunc defines the default generation language
|
|
DefaultLanguageFunc func() *LanguageOpts
|
|
|
|
moduleRe *regexp.Regexp
|
|
)
|
|
|
|
func initLanguage() {
|
|
DefaultLanguageFunc = GoLangOpts
|
|
|
|
moduleRe = regexp.MustCompile(`module[ \t]+([^\s]+)`)
|
|
}
|
|
|
|
// LanguageOpts to describe a language to the code generator
|
|
type LanguageOpts struct {
|
|
ReservedWords []string
|
|
BaseImportFunc func(string) string `json:"-"`
|
|
ImportsFunc func(map[string]string) string `json:"-"`
|
|
ArrayInitializerFunc func(interface{}) (string, error) `json:"-"`
|
|
reservedWordsSet map[string]struct{}
|
|
initialized bool
|
|
formatFunc func(string, []byte) ([]byte, error)
|
|
fileNameFunc func(string) string // language specific source file naming rules
|
|
dirNameFunc func(string) string // language specific directory naming rules
|
|
}
|
|
|
|
// Init the language option
|
|
func (l *LanguageOpts) Init() {
|
|
if l.initialized {
|
|
return
|
|
}
|
|
l.initialized = true
|
|
l.reservedWordsSet = make(map[string]struct{})
|
|
for _, rw := range l.ReservedWords {
|
|
l.reservedWordsSet[rw] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// MangleName makes sure a reserved word gets a safe name
|
|
func (l *LanguageOpts) MangleName(name, suffix string) string {
|
|
if _, ok := l.reservedWordsSet[swag.ToFileName(name)]; !ok {
|
|
return name
|
|
}
|
|
return strings.Join([]string{name, suffix}, "_")
|
|
}
|
|
|
|
// MangleVarName makes sure a reserved word gets a safe name
|
|
func (l *LanguageOpts) MangleVarName(name string) string {
|
|
nm := swag.ToVarName(name)
|
|
if _, ok := l.reservedWordsSet[nm]; !ok {
|
|
return nm
|
|
}
|
|
return nm + "Var"
|
|
}
|
|
|
|
// MangleFileName makes sure a file name gets a safe name
|
|
func (l *LanguageOpts) MangleFileName(name string) string {
|
|
if l.fileNameFunc != nil {
|
|
return l.fileNameFunc(name)
|
|
}
|
|
return swag.ToFileName(name)
|
|
}
|
|
|
|
// ManglePackageName makes sure a package gets a safe name.
|
|
// In case of a file system path (e.g. name contains "/" or "\" on Windows), this return only the last element.
|
|
func (l *LanguageOpts) ManglePackageName(name, suffix string) string {
|
|
if name == "" {
|
|
return suffix
|
|
}
|
|
if l.dirNameFunc != nil {
|
|
name = l.dirNameFunc(name)
|
|
}
|
|
pth := filepath.ToSlash(filepath.Clean(name)) // preserve path
|
|
pkg := importAlias(pth) // drop path
|
|
return l.MangleName(swag.ToFileName(prefixForName(pkg)+pkg), suffix)
|
|
}
|
|
|
|
// ManglePackagePath makes sure a full package path gets a safe name.
|
|
// Only the last part of the path is altered.
|
|
func (l *LanguageOpts) ManglePackagePath(name string, suffix string) string {
|
|
if name == "" {
|
|
return suffix
|
|
}
|
|
target := filepath.ToSlash(filepath.Clean(name)) // preserve path
|
|
parts := strings.Split(target, "/")
|
|
parts[len(parts)-1] = l.ManglePackageName(parts[len(parts)-1], suffix)
|
|
return strings.Join(parts, "/")
|
|
}
|
|
|
|
// FormatContent formats a file with a language specific formatter
|
|
func (l *LanguageOpts) FormatContent(name string, content []byte) ([]byte, error) {
|
|
if l.formatFunc != nil {
|
|
return l.formatFunc(name, content)
|
|
}
|
|
return content, nil
|
|
}
|
|
|
|
// imports generate the code to import some external packages, possibly aliased
|
|
func (l *LanguageOpts) imports(imports map[string]string) string {
|
|
if l.ImportsFunc != nil {
|
|
return l.ImportsFunc(imports)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// arrayInitializer builds a litteral array
|
|
func (l *LanguageOpts) arrayInitializer(data interface{}) (string, error) {
|
|
if l.ArrayInitializerFunc != nil {
|
|
return l.ArrayInitializerFunc(data)
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
// baseImport figures out the base path to generate import statements
|
|
func (l *LanguageOpts) baseImport(tgt string) string {
|
|
if l.BaseImportFunc != nil {
|
|
return l.BaseImportFunc(tgt)
|
|
}
|
|
debugLog("base import func is nil")
|
|
return ""
|
|
}
|
|
|
|
// GoLangOpts for rendering items as golang code
|
|
func GoLangOpts() *LanguageOpts {
|
|
var goOtherReservedSuffixes = map[string]bool{
|
|
// see:
|
|
// https://golang.org/src/go/build/syslist.go
|
|
// https://golang.org/doc/install/source#environment
|
|
|
|
// goos
|
|
"aix": true,
|
|
"android": true,
|
|
"darwin": true,
|
|
"dragonfly": true,
|
|
"freebsd": true,
|
|
"hurd": true,
|
|
"illumos": true,
|
|
"js": true,
|
|
"linux": true,
|
|
"nacl": true,
|
|
"netbsd": true,
|
|
"openbsd": true,
|
|
"plan9": true,
|
|
"solaris": true,
|
|
"windows": true,
|
|
"zos": true,
|
|
|
|
// arch
|
|
"386": true,
|
|
"amd64": true,
|
|
"amd64p32": true,
|
|
"arm": true,
|
|
"armbe": true,
|
|
"arm64": true,
|
|
"arm64be": true,
|
|
"mips": true,
|
|
"mipsle": true,
|
|
"mips64": true,
|
|
"mips64le": true,
|
|
"mips64p32": true,
|
|
"mips64p32le": true,
|
|
"ppc": true,
|
|
"ppc64": true,
|
|
"ppc64le": true,
|
|
"riscv": true,
|
|
"riscv64": true,
|
|
"s390": true,
|
|
"s390x": true,
|
|
"sparc": true,
|
|
"sparc64": true,
|
|
"wasm": true,
|
|
|
|
// other reserved suffixes
|
|
"test": true,
|
|
}
|
|
|
|
opts := new(LanguageOpts)
|
|
opts.ReservedWords = []string{
|
|
"break", "default", "func", "interface", "select",
|
|
"case", "defer", "go", "map", "struct",
|
|
"chan", "else", "goto", "package", "switch",
|
|
"const", "fallthrough", "if", "range", "type",
|
|
"continue", "for", "import", "return", "var",
|
|
}
|
|
|
|
opts.formatFunc = func(ffn string, content []byte) ([]byte, error) {
|
|
opts := new(imports.Options)
|
|
opts.TabIndent = true
|
|
opts.TabWidth = 2
|
|
opts.Fragment = true
|
|
opts.Comments = true
|
|
return imports.Process(ffn, content, opts)
|
|
}
|
|
|
|
opts.fileNameFunc = func(name string) string {
|
|
// whenever a generated file name ends with a suffix
|
|
// that is meaningful to go build, adds a "swagger"
|
|
// suffix
|
|
parts := strings.Split(swag.ToFileName(name), "_")
|
|
if goOtherReservedSuffixes[parts[len(parts)-1]] {
|
|
// file name ending with a reserved arch or os name
|
|
// are appended an innocuous suffix "swagger"
|
|
parts = append(parts, "swagger")
|
|
}
|
|
return strings.Join(parts, "_")
|
|
}
|
|
|
|
opts.dirNameFunc = func(name string) string {
|
|
// whenever a generated directory name is a special
|
|
// golang directory, append an innocuous suffix
|
|
switch name {
|
|
case "vendor", "internal":
|
|
return strings.Join([]string{name, "swagger"}, "_")
|
|
}
|
|
return name
|
|
}
|
|
|
|
opts.ImportsFunc = func(imports map[string]string) string {
|
|
if len(imports) == 0 {
|
|
return ""
|
|
}
|
|
result := make([]string, 0, len(imports))
|
|
for k, v := range imports {
|
|
_, name := path.Split(v)
|
|
if name != k {
|
|
result = append(result, fmt.Sprintf("\t%s %q", k, v))
|
|
} else {
|
|
result = append(result, fmt.Sprintf("\t%q", v))
|
|
}
|
|
}
|
|
sort.Strings(result)
|
|
return strings.Join(result, "\n")
|
|
}
|
|
|
|
opts.ArrayInitializerFunc = func(data interface{}) (string, error) {
|
|
// ArrayInitializer constructs a Go literal initializer from interface{} literals.
|
|
// e.g. []interface{}{"a", "b"} is transformed in {"a","b",}
|
|
// e.g. map[string]interface{}{ "a": "x", "b": "y"} is transformed in {"a":"x","b":"y",}.
|
|
//
|
|
// NOTE: this is currently used to construct simple slice intializers for default values.
|
|
// This allows for nicer slice initializers for slices of primitive types and avoid systematic use for json.Unmarshal().
|
|
b, err := json.Marshal(data)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.Replace(strings.Replace(strings.Replace(string(b), "}", ",}", -1), "[", "{", -1), "]", ",}", -1), nil
|
|
}
|
|
|
|
opts.BaseImportFunc = func(tgt string) string {
|
|
tgt = filepath.Clean(tgt)
|
|
// On Windows, filepath.Abs("") behaves differently than on Unix.
|
|
// Windows: yields an error, since Abs() does not know the volume.
|
|
// UNIX: returns current working directory
|
|
if tgt == "" {
|
|
tgt = "."
|
|
}
|
|
tgtAbsPath, err := filepath.Abs(tgt)
|
|
if err != nil {
|
|
log.Fatalf("could not evaluate base import path with target \"%s\": %v", tgt, err)
|
|
}
|
|
|
|
var tgtAbsPathExtended string
|
|
tgtAbsPathExtended, err = filepath.EvalSymlinks(tgtAbsPath)
|
|
if err != nil {
|
|
log.Fatalf("could not evaluate base import path with target \"%s\" (with symlink resolution): %v", tgtAbsPath, err)
|
|
}
|
|
|
|
gopath := os.Getenv("GOPATH")
|
|
if gopath == "" {
|
|
gopath = filepath.Join(os.Getenv("HOME"), "go")
|
|
}
|
|
|
|
var pth string
|
|
for _, gp := range filepath.SplitList(gopath) {
|
|
// EvalSymLinks also calls the Clean
|
|
gopathExtended, er := filepath.EvalSymlinks(gp)
|
|
if er != nil {
|
|
log.Fatalln(er)
|
|
}
|
|
gopathExtended = filepath.Join(gopathExtended, "src")
|
|
gp = filepath.Join(gp, "src")
|
|
|
|
// At this stage we have expanded and unexpanded target path. GOPATH is fully expanded.
|
|
// Expanded means symlink free.
|
|
// We compare both types of targetpath<s> with gopath.
|
|
// If any one of them coincides with gopath , it is imperative that
|
|
// target path lies inside gopath. How?
|
|
// - Case 1: Irrespective of symlinks paths coincide. Both non-expanded paths.
|
|
// - Case 2: Symlink in target path points to location inside GOPATH. (Expanded Target Path)
|
|
// - Case 3: Symlink in target path points to directory outside GOPATH (Unexpanded target path)
|
|
|
|
// Case 1: - Do nothing case. If non-expanded paths match just generate base import path as if
|
|
// there are no symlinks.
|
|
|
|
// Case 2: - Symlink in target path points to location inside GOPATH. (Expanded Target Path)
|
|
// First if will fail. Second if will succeed.
|
|
|
|
// Case 3: - Symlink in target path points to directory outside GOPATH (Unexpanded target path)
|
|
// First if will succeed and break.
|
|
|
|
// compares non expanded path for both
|
|
if ok, relativepath := checkPrefixAndFetchRelativePath(tgtAbsPath, gp); ok {
|
|
pth = relativepath
|
|
break
|
|
}
|
|
|
|
// Compares non-expanded target path
|
|
if ok, relativepath := checkPrefixAndFetchRelativePath(tgtAbsPath, gopathExtended); ok {
|
|
pth = relativepath
|
|
break
|
|
}
|
|
|
|
// Compares expanded target path.
|
|
if ok, relativepath := checkPrefixAndFetchRelativePath(tgtAbsPathExtended, gopathExtended); ok {
|
|
pth = relativepath
|
|
break
|
|
}
|
|
|
|
}
|
|
|
|
mod, goModuleAbsPath, err := tryResolveModule(tgtAbsPath)
|
|
switch {
|
|
case err != nil:
|
|
log.Fatalf("Failed to resolve module using go.mod file: %s", err)
|
|
case mod != "":
|
|
relTgt := relPathToRelGoPath(goModuleAbsPath, tgtAbsPath)
|
|
if !strings.HasSuffix(mod, relTgt) {
|
|
return filepath.ToSlash(mod + relTgt)
|
|
}
|
|
return filepath.ToSlash(mod)
|
|
}
|
|
|
|
if pth == "" {
|
|
log.Fatalln("target must reside inside a location in the $GOPATH/src or be a module")
|
|
}
|
|
return filepath.ToSlash(pth)
|
|
}
|
|
opts.Init()
|
|
return opts
|
|
}
|
|
|
|
// resolveGoModFile walks up the directory tree starting from 'dir' until it
|
|
// finds a go.mod file. If go.mod is found it will return the related file
|
|
// object. If no go.mod file is found it will return an error.
|
|
func resolveGoModFile(dir string) (*os.File, string, error) {
|
|
goModPath := filepath.Join(dir, "go.mod")
|
|
f, err := os.Open(goModPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) && dir != filepath.Dir(dir) {
|
|
return resolveGoModFile(filepath.Dir(dir))
|
|
}
|
|
return nil, "", err
|
|
}
|
|
return f, dir, nil
|
|
}
|
|
|
|
// relPathToRelGoPath takes a relative os path and returns the relative go
|
|
// package path. For unix nothing will change but for windows \ will be
|
|
// converted to /.
|
|
func relPathToRelGoPath(modAbsPath, absPath string) string {
|
|
if absPath == "." {
|
|
return ""
|
|
}
|
|
|
|
path := strings.TrimPrefix(absPath, modAbsPath)
|
|
pathItems := strings.Split(path, string(filepath.Separator))
|
|
return strings.Join(pathItems, "/")
|
|
}
|
|
|
|
func tryResolveModule(baseTargetPath string) (string, string, error) {
|
|
f, goModAbsPath, err := resolveGoModFile(baseTargetPath)
|
|
switch {
|
|
case os.IsNotExist(err):
|
|
return "", "", nil
|
|
case err != nil:
|
|
return "", "", err
|
|
}
|
|
|
|
src, err := ioutil.ReadAll(f)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
match := moduleRe.FindSubmatch(src)
|
|
if len(match) != 2 {
|
|
return "", "", nil
|
|
}
|
|
|
|
return string(match[1]), goModAbsPath, nil
|
|
}
|
|
|
|
// 1. Checks if the child path and parent path coincide.
|
|
// 2. If they do return child path relative to parent path.
|
|
// 3. Everything else return false
|
|
func checkPrefixAndFetchRelativePath(childpath string, parentpath string) (bool, string) {
|
|
// Windows (local) file systems - NTFS, as well as FAT and variants
|
|
// are case insensitive.
|
|
cp, pp := childpath, parentpath
|
|
if goruntime.GOOS == "windows" {
|
|
cp = strings.ToLower(cp)
|
|
pp = strings.ToLower(pp)
|
|
}
|
|
|
|
if strings.HasPrefix(cp, pp) {
|
|
pth, err := filepath.Rel(parentpath, childpath)
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
return true, pth
|
|
}
|
|
|
|
return false, ""
|
|
|
|
}
|