github.com/d0sbit/gocode@v0.0.0-20211001144653-a968ce917518/cmd/gocode_sqlcrud/main.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"embed"
     6  	"encoding/json"
     7  	"flag"
     8  	"fmt"
     9  	"io/fs"
    10  	"log"
    11  	"os"
    12  	"path"
    13  	"path/filepath"
    14  	"sort"
    15  	"strings"
    16  	"text/template"
    17  	"time"
    18  
    19  	"github.com/psanford/memfs"
    20  
    21  	"github.com/d0sbit/gocode/srcedit"
    22  	"github.com/d0sbit/gocode/srcedit/diff"
    23  	"github.com/d0sbit/gocode/srcedit/model"
    24  )
    25  
    26  //go:embed sqlcrud.tmpl
    27  var defaultTmplFS embed.FS
    28  
    29  func main() {
    30  	os.Exit(maine(
    31  		flag.NewFlagSet(os.Args[0], flag.PanicOnError),
    32  		os.Args[1:]))
    33  }
    34  
    35  // maine is broken out so it can be tested separately
    36  func maine(flagSet *flag.FlagSet, args []string) int {
    37  
    38  	typeF := flagSet.String("type", "", "Type name of Go struct with fields corresponding to the MongoDB document")
    39  	fileF := flagSet.String("file", "", "Filename for the main type into which to add store code")
    40  	testFileF := flagSet.String("test-file", "", "Test filename into which to add code, defaults to file with _test.go suffix")
    41  	storeFileF := flagSet.String("store-file", "store.go", "Filename for the Store type")
    42  	storeTestFileF := flagSet.String("store-test-file", "store_test.go", "Filename for the Store type tests")
    43  	packageF := flagSet.String("package", "", "Package directory within module to analyze/edit")
    44  	migrationsPackageF := flagSet.String("migrations-package", "", "Package directory to use for migrations, will default to ../migrations resolved against the package directory")
    45  	dryRunF := flagSet.String("dry-run", "off", "Do not apply changes, only output diff of what would change. Value specifies format, 'term' for terminal pretty text, 'html' for HTML, or 'off' to disable.")
    46  	noGofmtF := flagSet.Bool("no-gofmt", false, "Do not gofmt the output")
    47  	jsonF := flagSet.Bool("json", false, "Write output as JSON")
    48  	vF := flagSet.Bool("v", false, "Verbose output")
    49  	// allF := flagSet.Bool("all", false, "Generate all methods")
    50  
    51  	flagSet.Parse(args)
    52  
    53  	fileArgList := flagSet.Args()
    54  	if len(fileArgList) > 1 {
    55  		log.Fatalf("you cannot specify more than one file (%d found)", len(fileArgList))
    56  	}
    57  
    58  	// FIXME: for now if they provide an arg we just infer the various other params and overwrite them,
    59  	// this should be cleaned up at some point so it is consistent with gocode_handlercrud and whatever else
    60  	if len(fileArgList) == 1 {
    61  		fileArg := fileArgList[0]
    62  
    63  		rootFS, modDir, packagePath, modPath, err := srcedit.FindOSWdModuleDir(filepath.Dir(fileArg))
    64  		if err != nil {
    65  			log.Fatalf("error finding module directory: %v", err)
    66  		}
    67  		// if *vF {
    68  		// log.Printf("file arg step: rootFS=%v; modDir=%q, packagePath=%q, modPath=%q", rootFS, modDir, packagePath, modPath)
    69  		// }
    70  		inFS, err := fs.Sub(rootFS, modDir)
    71  		if err != nil {
    72  			log.Fatalf("fs.Sub error while construct input fs: %v", err)
    73  		}
    74  
    75  		// we won't be writing here so don't bother with outFS
    76  		storePkg := srcedit.NewPackage(inFS, inFS, modPath, packagePath)
    77  
    78  		// now that we have all of this stuff hacked in here,
    79  		// we can look for the type based on the file name
    80  		typeSearch := strings.TrimSuffix(filepath.Base(fileArg), ".go")
    81  		typeInfo, err := storePkg.FindTypeLoose(typeSearch)
    82  		if err != nil {
    83  			log.Fatalf("failed to find type for %q: %v", typeSearch, err)
    84  		}
    85  
    86  		// ast.Print(typeInfo.FileSet, typeInfo.GenDecl)
    87  		*typeF = typeInfo.Name()
    88  
    89  		*fileF = filepath.Base(fileArg)
    90  
    91  		// testFileF can just be inferred below
    92  		// storeFileF and storeTestFileF are fine as-is too
    93  
    94  		// packageF needs to come from FindOSWdModuleDir
    95  		*packageF = packagePath
    96  
    97  	}
    98  
    99  	typeName := *typeF
   100  	if typeName == "" {
   101  		log.Fatalf("-type is required")
   102  	}
   103  
   104  	typeFilename := *fileF
   105  	if typeFilename == "" {
   106  		typeFilename = srcedit.LowerForType(typeName+"Store", "-") + ".go"
   107  	}
   108  
   109  	if *testFileF == "" {
   110  		*testFileF = strings.TrimSuffix(typeFilename, ".go") + "_test.go"
   111  	}
   112  
   113  	rootFS, modDir, packagePath, modPath, err := srcedit.FindOSWdModuleDir(*packageF)
   114  	if err != nil {
   115  		log.Fatalf("error finding module directory: %v", err)
   116  	}
   117  	if *vF {
   118  		log.Printf("rootFS=%v; modDir=%q, packagePath=%q, modPath=%q", rootFS, modDir, packagePath, modPath)
   119  	}
   120  
   121  	migrationsPackagePath := *migrationsPackageF
   122  	if migrationsPackagePath == "" {
   123  		migrationsPackagePath = strings.TrimPrefix(path.Join(packagePath, "../migrations"), "/")
   124  	}
   125  
   126  	// set up file systems
   127  	inFS, err := fs.Sub(rootFS, modDir)
   128  	if err != nil {
   129  		log.Fatalf("fs.Sub error while construct input fs: %v", err)
   130  	}
   131  
   132  	// output is either same as input or memory for dry-run
   133  	var outFS fs.FS
   134  	var dryRunFS *memfs.FS
   135  	if *dryRunF == "off" {
   136  		outFS = inFS
   137  		if migrationsPackagePath != "" {
   138  			mda, ok := outFS.(srcedit.MkdirAller)
   139  			if ok {
   140  				err := mda.MkdirAll(migrationsPackagePath, 0755)
   141  				if err != nil {
   142  					log.Fatalf("mda.MkdirAll for %q: %v", migrationsPackagePath, err)
   143  				}
   144  			}
   145  			//outFS.MkdirAll(migrationsPackagePath, 0755)
   146  		}
   147  	} else {
   148  		dryRunFS = memfs.New()
   149  		if packagePath != "" {
   150  			dryRunFS.MkdirAll(packagePath, 0755)
   151  		}
   152  		if migrationsPackagePath != "" {
   153  			// log.Printf("creating migrationsPackagePath %q", migrationsPackagePath)
   154  			err := dryRunFS.MkdirAll(migrationsPackagePath, 0755)
   155  			// log.Printf("migrations MkdirAll returned: %v", err)
   156  			_ = err
   157  		}
   158  		outFS = dryRunFS
   159  	}
   160  
   161  	// load the package with srcedit
   162  	pkg := srcedit.NewPackage(inFS, outFS, modPath, packagePath)
   163  
   164  	// load the migrations package
   165  	if *vF {
   166  		log.Printf("NewPackage for migrations: inFS=%#v, outFS=%#v, modPath=%#v, migrationsPackagePath=%#v",
   167  			inFS, outFS, modPath, migrationsPackagePath)
   168  	}
   169  	migrationsPkg := srcedit.NewPackage(inFS, outFS, modPath, migrationsPackagePath)
   170  
   171  	// get the definition for the specified type
   172  	typeInfo, err := pkg.FindType(typeName)
   173  	if err != nil {
   174  		log.Fatalf("failed to find type %q: %v", typeName, err)
   175  	}
   176  
   177  	// populate Struct
   178  	// s := Struct{
   179  	// 	pkgImportedName: "", // TOOD: figure out what to do with prefixed types (not in the same package)
   180  	// 	name:            typeName,
   181  	// 	typeInfo:        typeInfo,
   182  	// }
   183  	// s.fields, err = s.makeFields()
   184  	// if err != nil {
   185  	// 	log.Fatalf("failed to extract field info from type %q: %v", typeName, err)
   186  	// }
   187  
   188  	s, err := model.NewStruct(typeInfo, "")
   189  	if err != nil {
   190  		log.Fatalf("failed to find type %q: %v", typeName, err)
   191  	}
   192  
   193  	// execute template
   194  	data := struct {
   195  		Struct               *model.Struct
   196  		MigrationsImportPath string
   197  	}{
   198  		Struct:               s,
   199  		MigrationsImportPath: modPath + "/" + migrationsPackagePath,
   200  	}
   201  	tmpl, err := template.New("_main_").Funcs(funcMap).ParseFS(defaultTmplFS, "sqlcrud.tmpl")
   202  	if err != nil {
   203  		log.Fatalf("template parse error: %v", err)
   204  	}
   205  
   206  	fmtt := &srcedit.GofmtTransform{}
   207  	var trs []srcedit.Transform
   208  
   209  	{
   210  		fn := "sqlutil.go"
   211  		fmtt.FilenameList = append(fmtt.FilenameList, fn)
   212  		trList, err := tmplToTransforms(fn, data, tmpl, "SQLUtil")
   213  		if err != nil {
   214  			log.Fatal(err)
   215  		}
   216  		trs = append(trs, trList...)
   217  	}
   218  
   219  	{
   220  		fn := *storeFileF
   221  		fmtt.FilenameList = append(fmtt.FilenameList, fn)
   222  		trList, err := tmplToTransforms(fn, data, tmpl, "Store", "StoreMethods")
   223  		if err != nil {
   224  			log.Fatal(err)
   225  		}
   226  		trs = append(trs, trList...)
   227  	}
   228  
   229  	{
   230  		fn := *storeTestFileF
   231  		fmtt.FilenameList = append(fmtt.FilenameList, fn)
   232  		trList, err := tmplToTransforms(fn, data, tmpl, "TestStore")
   233  		if err != nil {
   234  			log.Fatal(err)
   235  		}
   236  		trs = append(trs, trList...)
   237  	}
   238  
   239  	{
   240  		fn := typeFilename
   241  		fmtt.FilenameList = append(fmtt.FilenameList, fn)
   242  		trList, err := tmplToTransforms(fn, data, tmpl,
   243  			"TYPEStore",
   244  			"TYPEStoreMethods",
   245  			// FIXME: filter which things go here base on flags
   246  			"TYPEInsert",
   247  			"TYPEDelete",
   248  			"TYPEUpdate",
   249  			"TYPESelectByID",
   250  			"TYPESelect",
   251  			"TYPESelectCursor",
   252  			"TYPECount",
   253  		)
   254  		if err != nil {
   255  			log.Fatal(err)
   256  		}
   257  		trs = append(trs, trList...)
   258  	}
   259  
   260  	{
   261  		// TODO: -no-test flag
   262  		fn := *testFileF
   263  		fmtt.FilenameList = append(fmtt.FilenameList, fn)
   264  		trList, err := tmplToTransforms(fn, data, tmpl,
   265  			"TestTYPE",
   266  		)
   267  		if err != nil {
   268  			log.Fatal(err)
   269  		}
   270  		trs = append(trs, trList...)
   271  	}
   272  
   273  	dd := &srcedit.DedupImportsTransform{
   274  		FilenameList: fmtt.FilenameList,
   275  	}
   276  	trs = append(trs, dd)
   277  
   278  	if !*noGofmtF {
   279  		trs = append(trs, fmtt)
   280  	}
   281  
   282  	err = pkg.ApplyTransforms(trs...)
   283  	if err != nil {
   284  		log.Fatalf("apply transform error: %v", err)
   285  	}
   286  
   287  	// TODO: option to skip migrations stuff?
   288  	// do migrations package separately
   289  	{
   290  		fmtt := &srcedit.GofmtTransform{}
   291  		var trs []srcedit.Transform
   292  
   293  		fn := "migrations.go"
   294  		fmtt.FilenameList = append(fmtt.FilenameList, fn)
   295  		trList, err := tmplToTransforms(fn, data, tmpl, "Migrations")
   296  		if err != nil {
   297  			log.Fatal(err)
   298  		}
   299  		trs = append(trs, trList...)
   300  
   301  		dd := &srcedit.DedupImportsTransform{
   302  			FilenameList: fmtt.FilenameList,
   303  		}
   304  		trs = append(trs, dd)
   305  
   306  		if !*noGofmtF {
   307  			trs = append(trs, fmtt)
   308  		}
   309  
   310  		err = migrationsPkg.ApplyTransforms(trs...)
   311  		if err != nil {
   312  			log.Fatalf("apply transform for migrations error: %v", err)
   313  		}
   314  
   315  		// mpf, err := inFS.Open(migrationsPackagePath)
   316  
   317  		needSampleMigration := true
   318  		err = fs.WalkDir(inFS, migrationsPackagePath, fs.WalkDirFunc(func(path string, d fs.DirEntry, err error) error {
   319  			if err != nil {
   320  				return err
   321  			}
   322  			if path == migrationsPackagePath {
   323  				return nil
   324  			}
   325  			if d.IsDir() { // only scan the immediate directory
   326  				return fs.SkipDir
   327  			}
   328  			if strings.HasSuffix(path, ".sql") || strings.HasSuffix(path, ".SQL") {
   329  				needSampleMigration = false
   330  			}
   331  			return nil
   332  		}))
   333  		if err != nil && !os.IsNotExist(err) {
   334  			log.Fatalf("error walking migrations dir %q: %v", migrationsPackagePath, err)
   335  		}
   336  
   337  		if needSampleMigration {
   338  			b := []byte(`
   339  -- +goose Up
   340  
   341  -- +goose Down
   342  
   343  `)
   344  			fname := time.Now().UTC().Format("20060102150405") + "_sample.sql"
   345  			fpath := path.Join(migrationsPackagePath, fname)
   346  			err := outFS.(srcedit.FileWriter).WriteFile(fpath, b, 0644)
   347  			if err != nil {
   348  				log.Fatalf("error creating example migration file %q: %v", fpath, err)
   349  			}
   350  		}
   351  
   352  	}
   353  
   354  	if *dryRunF != "off" {
   355  		diffMap, err := diff.Run(inFS, outFS, ".", *dryRunF)
   356  		if err != nil {
   357  			log.Fatalf("error running diff: %v", err)
   358  		}
   359  		if *jsonF {
   360  			enc := json.NewEncoder(os.Stdout)
   361  			enc.Encode(map[string]interface{}{
   362  				"diff": diffMap,
   363  			})
   364  		} else {
   365  			klist := make([]string, 0, len(diffMap))
   366  			for k := range diffMap {
   367  				klist = append(klist, k)
   368  			}
   369  			sort.Strings(klist)
   370  			for _, k := range klist {
   371  				fmt.Printf("### %s\n", k)
   372  				fmt.Println(diffMap[k])
   373  			}
   374  		}
   375  	}
   376  
   377  	return 0
   378  }
   379  
   380  func tmplToTransforms(fileName string, data interface{}, tmpl *template.Template, tmplName ...string) ([]srcedit.Transform, error) {
   381  
   382  	var ret []srcedit.Transform
   383  
   384  	for _, tName := range tmplName {
   385  		var buf bytes.Buffer
   386  		err := tmpl.ExecuteTemplate(&buf, tName, data)
   387  		if err != nil {
   388  			return ret, fmt.Errorf("%q template exec error: %v", tName, err)
   389  		}
   390  
   391  		trList, err := srcedit.ParseTransforms(fileName, buf.String())
   392  		if err != nil {
   393  			return ret, fmt.Errorf("%q transform parse error: %v", tName, err)
   394  		}
   395  		ret = append(ret, trList...)
   396  
   397  	}
   398  
   399  	return ret, nil
   400  
   401  }
   402  
   403  var funcMap = template.FuncMap(map[string]interface{}{
   404  	"LowerForType": srcedit.LowerForType,
   405  })