github.com/Ali-iotechsys/sqlboiler/v4@v4.0.0-20221208124957-6aec9a5f1f71/boilingcore/output.go (about)

     1  package boilingcore
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"go/format"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"strconv"
    12  	"strings"
    13  	"text/template"
    14  
    15  	"github.com/friendsofgo/errors"
    16  	"github.com/volatiletech/sqlboiler/v4/importers"
    17  )
    18  
    19  // Copied from the go source
    20  // see: https://github.com/golang/go/blob/master/src/go/build/syslist.go
    21  var (
    22  	goosList = stringSliceToMap(strings.Fields("aix android darwin dragonfly freebsd hurd illumos ios js linux nacl netbsd openbsd plan9 solaris windows zos"))
    23  
    24  	goarchList = stringSliceToMap(strings.Fields("386 amd64 amd64p32 arm armbe arm64 arm64be loong64 mips mipsle mips64 mips64le mips64p32 mips64p32le ppc ppc64 ppc64le riscv riscv64 s390 s390x sparc sparc64 wasm"))
    25  )
    26  
    27  var (
    28  	noEditDisclaimerFmt = `// Code generated by SQLBoiler%s(https://github.com/volatiletech/sqlboiler). DO NOT EDIT.
    29  // This file is meant to be re-generated in place and/or deleted at any time.
    30  
    31  `
    32  	noEditDisclaimer = []byte(fmt.Sprintf(noEditDisclaimerFmt, " "))
    33  )
    34  
    35  var (
    36  	// templateByteBuffer is re-used by all template construction to avoid
    37  	// allocating more memory than is needed. This will later be a problem for
    38  	// concurrency, address it then.
    39  	templateByteBuffer = &bytes.Buffer{}
    40  
    41  	rgxRemoveNumberedPrefix = regexp.MustCompile(`^[0-9]+_`)
    42  	rgxSyntaxError          = regexp.MustCompile(`(\d+):\d+: `)
    43  
    44  	testHarnessWriteFile = os.WriteFile
    45  )
    46  
    47  type executeTemplateData struct {
    48  	state *State
    49  	data  *templateData
    50  
    51  	templates     *templateList
    52  	dirExtensions dirExtMap
    53  
    54  	importSet      importers.Set
    55  	importNamedSet importers.Map
    56  
    57  	combineImportsOnType bool
    58  	isTest               bool
    59  }
    60  
    61  // generateOutput builds the file output and sends it to outHandler for saving
    62  func generateOutput(state *State, dirExts dirExtMap, data *templateData) error {
    63  	return executeTemplates(executeTemplateData{
    64  		state:                state,
    65  		data:                 data,
    66  		templates:            state.Templates,
    67  		importSet:            state.Config.Imports.All,
    68  		combineImportsOnType: true,
    69  		dirExtensions:        dirExts,
    70  	})
    71  }
    72  
    73  // generateTestOutput builds the test file output and sends it to outHandler for saving
    74  func generateTestOutput(state *State, dirExts dirExtMap, data *templateData) error {
    75  	return executeTemplates(executeTemplateData{
    76  		state:                state,
    77  		data:                 data,
    78  		templates:            state.TestTemplates,
    79  		importSet:            state.Config.Imports.Test,
    80  		combineImportsOnType: false,
    81  		isTest:               true,
    82  		dirExtensions:        dirExts,
    83  	})
    84  }
    85  
    86  // generateSingletonOutput processes the templates that should only be run
    87  // one time.
    88  func generateSingletonOutput(state *State, data *templateData) error {
    89  	return executeSingletonTemplates(executeTemplateData{
    90  		state:          state,
    91  		data:           data,
    92  		templates:      state.Templates,
    93  		importNamedSet: state.Config.Imports.Singleton,
    94  	})
    95  }
    96  
    97  // generateSingletonTestOutput processes the templates that should only be run
    98  // one time.
    99  func generateSingletonTestOutput(state *State, data *templateData) error {
   100  	return executeSingletonTemplates(executeTemplateData{
   101  		state:          state,
   102  		data:           data,
   103  		templates:      state.TestTemplates,
   104  		importNamedSet: state.Config.Imports.TestSingleton,
   105  		isTest:         true,
   106  	})
   107  }
   108  
   109  func executeTemplates(e executeTemplateData) error {
   110  	if e.data.Table.IsJoinTable {
   111  		return nil
   112  	}
   113  
   114  	var imps importers.Set
   115  	imps.Standard = e.importSet.Standard
   116  	imps.ThirdParty = e.importSet.ThirdParty
   117  	if e.combineImportsOnType {
   118  		colTypes := make([]string, len(e.data.Table.Columns))
   119  		for i, ct := range e.data.Table.Columns {
   120  			colTypes[i] = ct.Type
   121  		}
   122  
   123  		imps = importers.AddTypeImports(imps, e.state.Config.Imports.BasedOnType, colTypes)
   124  	}
   125  
   126  	for dir, dirExts := range e.dirExtensions {
   127  		for ext, tplNames := range dirExts {
   128  			out := templateByteBuffer
   129  			out.Reset()
   130  
   131  			isGo := filepath.Ext(ext) == ".go"
   132  			if isGo {
   133  				pkgName := e.state.Config.PkgName
   134  				if len(dir) != 0 {
   135  					pkgName = filepath.Base(dir)
   136  				}
   137  				writeFileDisclaimer(out)
   138  				writePackageName(out, pkgName)
   139  				writeImports(out, imps)
   140  			}
   141  
   142  			prevLen := out.Len()
   143  			for _, tplName := range tplNames {
   144  				if err := executeTemplate(out, e.templates.Template, tplName, e.data); err != nil {
   145  					return err
   146  				}
   147  			}
   148  
   149  			fName := getOutputFilename(e.data.Table.Name, e.isTest, isGo)
   150  			fName += ext
   151  			if len(dir) != 0 {
   152  				fName = filepath.Join(dir, fName)
   153  			}
   154  
   155  			// Skip writing the file if the content is empty
   156  			if out.Len()-prevLen < 1 {
   157  				fmt.Fprintf(os.Stderr, "skipping empty file: %s/%s\n", e.state.Config.OutFolder, fName)
   158  				continue
   159  			}
   160  
   161  			if err := writeFile(e.state.Config.OutFolder, fName, out, isGo); err != nil {
   162  				return err
   163  			}
   164  		}
   165  	}
   166  
   167  	return nil
   168  }
   169  
   170  func executeSingletonTemplates(e executeTemplateData) error {
   171  	if e.data.Table.IsJoinTable {
   172  		return nil
   173  	}
   174  
   175  	out := templateByteBuffer
   176  	for _, tplName := range e.templates.Templates() {
   177  		normalized, isSingleton, isGo, usePkg := outputFilenameParts(tplName)
   178  		if !isSingleton {
   179  			continue
   180  		}
   181  
   182  		dir, fName := filepath.Split(normalized)
   183  		fName = fName[:strings.IndexByte(fName, '.')]
   184  
   185  		out.Reset()
   186  
   187  		if isGo {
   188  			imps := importers.Set{
   189  				Standard:   e.importNamedSet[denormalizeSlashes(fName)].Standard,
   190  				ThirdParty: e.importNamedSet[denormalizeSlashes(fName)].ThirdParty,
   191  			}
   192  
   193  			pkgName := e.state.Config.PkgName
   194  			if !usePkg {
   195  				pkgName = filepath.Base(dir)
   196  			}
   197  			writeFileDisclaimer(out)
   198  			writePackageName(out, pkgName)
   199  			writeImports(out, imps)
   200  		}
   201  
   202  		if err := executeTemplate(out, e.templates.Template, tplName, e.data); err != nil {
   203  			return err
   204  		}
   205  
   206  		if err := writeFile(e.state.Config.OutFolder, normalized, out, isGo); err != nil {
   207  			return err
   208  		}
   209  	}
   210  
   211  	return nil
   212  }
   213  
   214  // writeFileDisclaimer writes the disclaimer at the top with a trailing
   215  // newline so the package name doesn't get attached to it.
   216  func writeFileDisclaimer(out *bytes.Buffer) {
   217  	_, _ = out.Write(noEditDisclaimer)
   218  }
   219  
   220  // writePackageName writes the package name correctly, ignores errors
   221  // since it's to the concrete buffer type which produces none
   222  func writePackageName(out *bytes.Buffer, pkgName string) {
   223  	_, _ = fmt.Fprintf(out, "package %s\n\n", pkgName)
   224  }
   225  
   226  // writeImports writes the package imports correctly, ignores errors
   227  // since it's to the concrete buffer type which produces none
   228  func writeImports(out *bytes.Buffer, imps importers.Set) {
   229  	if impStr := imps.Format(); len(impStr) > 0 {
   230  		_, _ = fmt.Fprintf(out, "%s\n", impStr)
   231  	}
   232  }
   233  
   234  // writeFile writes to the given folder and filename, formatting the buffer
   235  // given.
   236  func writeFile(outFolder string, fileName string, input *bytes.Buffer, format bool) error {
   237  	var byt []byte
   238  	var err error
   239  	if format {
   240  		byt, err = formatBuffer(input)
   241  		if err != nil {
   242  			return err
   243  		}
   244  	} else {
   245  		byt = input.Bytes()
   246  	}
   247  
   248  	path := filepath.Join(outFolder, fileName)
   249  	if err := testHarnessWriteFile(path, byt, 0664); err != nil {
   250  		return errors.Wrapf(err, "failed to write output file %s", path)
   251  	}
   252  
   253  	return nil
   254  }
   255  
   256  // executeTemplate takes a template and returns the output of the template
   257  // execution.
   258  func executeTemplate(buf *bytes.Buffer, t *template.Template, name string, data *templateData) (err error) {
   259  	defer func() {
   260  		if r := recover(); r != nil {
   261  			err = errors.Errorf("failed to execute template: %s\npanic: %+v\n", name, r)
   262  		}
   263  	}()
   264  
   265  	if err := t.ExecuteTemplate(buf, name, data); err != nil {
   266  		return errors.Wrapf(err, "failed to execute template: %s", name)
   267  	}
   268  	return nil
   269  }
   270  
   271  func formatBuffer(buf *bytes.Buffer) ([]byte, error) {
   272  	output, err := format.Source(buf.Bytes())
   273  	if err == nil {
   274  		return output, nil
   275  	}
   276  
   277  	matches := rgxSyntaxError.FindStringSubmatch(err.Error())
   278  	if matches == nil {
   279  		return nil, errors.Wrap(err, "failed to format template")
   280  	}
   281  
   282  	lineNum, _ := strconv.Atoi(matches[1])
   283  	scanner := bufio.NewScanner(buf)
   284  	errBuf := &bytes.Buffer{}
   285  	line := 1
   286  	for ; scanner.Scan(); line++ {
   287  		if delta := line - lineNum; delta < -5 || delta > 5 {
   288  			continue
   289  		}
   290  
   291  		if line == lineNum {
   292  			errBuf.WriteString(">>>> ")
   293  		} else {
   294  			fmt.Fprintf(errBuf, "% 4d ", line)
   295  		}
   296  		errBuf.Write(scanner.Bytes())
   297  		errBuf.WriteByte('\n')
   298  	}
   299  
   300  	return nil, errors.Wrapf(err, "failed to format template\n\n%s\n", errBuf.Bytes())
   301  }
   302  
   303  func getLongExt(filename string) string {
   304  	index := strings.IndexByte(filename, '.')
   305  	return filename[index:]
   306  }
   307  
   308  func getOutputFilename(tableName string, isTest, isGo bool) string {
   309  	if strings.HasPrefix(tableName, "_") {
   310  		tableName = "und" + tableName
   311  	}
   312  
   313  	if isGo && endsWithSpecialSuffix(tableName) {
   314  		tableName += "_model"
   315  	}
   316  
   317  	if isTest {
   318  		tableName += "_test"
   319  	}
   320  
   321  	return tableName
   322  }
   323  
   324  // See: https://pkg.go.dev/cmd/go#hdr-Build_constraints
   325  func endsWithSpecialSuffix(tableName string) bool {
   326  	parts := strings.Split(tableName, "_")
   327  
   328  	// Not enough parts to have a special suffix
   329  	if len(parts) < 2 {
   330  		return false
   331  	}
   332  
   333  	lastPart := parts[len(parts)-1]
   334  
   335  	if lastPart == "test" {
   336  		return true
   337  	}
   338  
   339  	if _, ok := goosList[lastPart]; ok {
   340  		return true
   341  	}
   342  
   343  	if _, ok := goarchList[lastPart]; ok {
   344  		return true
   345  	}
   346  
   347  	return false
   348  }
   349  
   350  func stringSliceToMap(slice []string) map[string]struct{} {
   351  	Map := make(map[string]struct{}, len(slice))
   352  	for _, v := range slice {
   353  		Map[v] = struct{}{}
   354  	}
   355  
   356  	return Map
   357  }
   358  
   359  // fileFragments will take something of the form:
   360  // templates/singleton/hello.go.tpl
   361  // templates_test/js/hello.js.tpl
   362  func outputFilenameParts(filename string) (normalized string, isSingleton, isGo, usePkg bool) {
   363  	fragments := strings.Split(filename, string(os.PathSeparator))
   364  	isSingleton = fragments[len(fragments)-2] == "singleton"
   365  
   366  	var remainingFragments []string
   367  	for _, f := range fragments[1:] {
   368  		if f != "singleton" {
   369  			remainingFragments = append(remainingFragments, f)
   370  		}
   371  	}
   372  
   373  	newFilename := remainingFragments[len(remainingFragments)-1]
   374  	newFilename = strings.TrimSuffix(newFilename, ".tpl")
   375  	newFilename = rgxRemoveNumberedPrefix.ReplaceAllString(newFilename, "")
   376  	remainingFragments[len(remainingFragments)-1] = newFilename
   377  
   378  	ext := filepath.Ext(newFilename)
   379  	isGo = ext == ".go"
   380  
   381  	usePkg = len(remainingFragments) == 1
   382  	normalized = strings.Join(remainingFragments, string(os.PathSeparator))
   383  
   384  	return normalized, isSingleton, isGo, usePkg
   385  }