1
0
mirror of https://github.com/raseels-repos/golang-saas-starter-kit.git synced 2025-06-08 23:56:37 +02:00

379 lines
11 KiB
Go

package dbtable2crud
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/schema"
"geeks-accelerator/oss/saas-starter-kit/example-project/tools/truss/internal/goparse"
"github.com/dustin/go-humanize/english"
"github.com/fatih/camelcase"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"github.com/sergi/go-diff/diffmatchpatch"
)
// Run in the main entry point for the dbtable2crud cmd.
func Run(db *sqlx.DB, log *log.Logger, dbName, dbTable, modelFile, modelName, templateDir, goSrcPath string) error {
log.SetPrefix(log.Prefix() + " : dbtable2crud")
// Ensure the schema is up to date
if err := schema.Migrate(db, log); err != nil {
return err
}
// When dbTable is empty, lower case the model name
if dbTable == "" {
dbTable = strings.Join(camelcase.Split(modelName), " ")
dbTable = english.PluralWord(2, dbTable, "")
dbTable = strings.Replace(dbTable, " ", "_", -1)
dbTable = strings.ToLower(dbTable)
}
// Parse the model file and load the specified model struct.
model, err := parseModelFile(db, log, dbName, dbTable, modelFile, modelName)
if err != nil {
return err
}
// Basic lint of the model struct.
err = validateModel(log, model)
if err != nil {
return err
}
tmplData := map[string]interface{}{
"GoSrcPath": goSrcPath,
}
// Update the model file with new or updated code.
err = updateModel(log, model, templateDir, tmplData)
if err != nil {
return err
}
// Update the model crud file with new or updated code.
err = updateModelCrud(db, log, dbName, dbTable, modelFile, modelName, templateDir, model, tmplData)
if err != nil {
return err
}
return nil
}
// validateModel performs a basic lint of the model struct to ensure
// code gen output is correct.
func validateModel(log *log.Logger, model *modelDef) error {
for _, sf := range model.Fields {
if sf.DbColumn == nil && sf.ColumnName != "-" {
log.Printf("validateStruct : Unable to find struct field for db column %s\n", sf.ColumnName)
}
var expectedType string
switch sf.FieldName {
case "ID":
expectedType = "string"
case "CreatedAt":
expectedType = "time.Time"
case "UpdatedAt":
expectedType = "time.Time"
case "ArchivedAt":
expectedType = "pq.NullTime"
}
if expectedType != "" && expectedType != sf.FieldType {
log.Printf("validateStruct : Struct field %s should be of type %s not %s\n", sf.FieldName, expectedType, sf.FieldType)
}
}
return nil
}
// updateModel updated the parsed code file with the new code.
func updateModel(log *log.Logger, model *modelDef, templateDir string, tmplData map[string]interface{}) error {
// Execute template and parse code to be used to compare against modelFile.
tmplObjs, err := loadTemplateObjects(log, model, templateDir, "models.tmpl", tmplData)
if err != nil {
return err
}
// Store the current code as a string to produce a diff.
curCode := model.String()
objHeaders := []*goparse.GoObject{}
for _, obj := range tmplObjs {
if obj.Type == goparse.GoObjectType_Comment || obj.Type == goparse.GoObjectType_LineBreak {
objHeaders = append(objHeaders, obj)
continue
}
if model.HasType(obj.Name, obj.Type) {
cur := model.Objects().Get(obj.Name, obj.Type)
newObjs := []*goparse.GoObject{}
if len(objHeaders) > 0 {
// Remove any comments and linebreaks before the existing object so updates can be added.
removeObjs := []*goparse.GoObject{}
for idx := cur.Index - 1; idx > 0; idx-- {
prevObj := model.Objects().List()[idx]
if prevObj.Type == goparse.GoObjectType_Comment || prevObj.Type == goparse.GoObjectType_LineBreak {
removeObjs = append(removeObjs, prevObj)
} else {
break
}
}
if len(removeObjs) > 0 {
err := model.Objects().Remove(removeObjs...)
if err != nil {
err = errors.WithMessagef(err, "Failed to update object %s %s for %s", obj.Type, obj.Name, model.Name)
return err
}
// Make sure the current index is correct.
cur = model.Objects().Get(obj.Name, obj.Type)
}
// Append comments and line breaks before adding the object
for _, c := range objHeaders {
newObjs = append(newObjs, c)
}
}
newObjs = append(newObjs, obj)
// Do the object replacement.
err := model.Objects().Replace(cur, newObjs...)
if err != nil {
err = errors.WithMessagef(err, "Failed to update object %s %s for %s", obj.Type, obj.Name, model.Name)
return err
}
} else {
// Append comments and line breaks before adding the object
for _, c := range objHeaders {
err := model.Objects().Add(c)
if err != nil {
err = errors.WithMessagef(err, "Failed to add object %s %s for %s", c.Type, c.Name, model.Name)
return err
}
}
err := model.Objects().Add(obj)
if err != nil {
err = errors.WithMessagef(err, "Failed to add object %s %s for %s", obj.Type, obj.Name, model.Name)
return err
}
}
objHeaders = []*goparse.GoObject{}
}
// Set some flags to determine additional imports and need to be added.
var hasEnum bool
var hasPq bool
for _, f := range model.Fields {
if f.DbColumn != nil && f.DbColumn.IsEnum {
hasEnum = true
}
if strings.HasPrefix(strings.Trim(f.FieldType, "*"), "pq.") {
hasPq = true
}
}
reqImports := []string{}
if hasEnum {
reqImports = append(reqImports, "database/sql/driver")
reqImports = append(reqImports, "gopkg.in/go-playground/validator.v9")
reqImports = append(reqImports, "github.com/pkg/errors")
}
if hasPq {
reqImports = append(reqImports, "github.com/lib/pq")
}
for _, in := range reqImports {
err := model.AddImport(goparse.GoImport{Name: in})
if err != nil {
err = errors.WithMessagef(err, "Failed to add import %s for %s", in, model.Name)
return err
}
}
// Produce a diff after the updates have been applied.
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(curCode, model.String(), true)
fmt.Println(dmp.DiffPrettyText(diffs))
return nil
}
// updateModelCrud updated the parsed code file with the new code.
func updateModelCrud(db *sqlx.DB, log *log.Logger, dbName, dbTable, modelFile, modelName, templateDir string, baseModel *modelDef, tmplData map[string]interface{}) error {
modelDir := filepath.Dir(modelFile)
crudFile := filepath.Join(modelDir, FormatCamelLowerUnderscore(baseModel.Name)+".go")
var crudDoc *goparse.GoDocument
if _, err := os.Stat(crudFile); os.IsNotExist(err) {
crudDoc, err = goparse.NewGoDocument(baseModel.Package)
if err != nil {
return err
}
} else {
// Parse the supplied model file.
crudDoc, err = goparse.ParseFile(log, modelFile)
if err != nil {
return err
}
}
// Load all the updated struct fields from the base model file.
structFields := make(map[string]map[string]modelField)
for _, obj := range baseModel.GoDocument.Objects().List() {
if obj.Type != goparse.GoObjectType_Struct || obj.Name == baseModel.Name {
continue
}
objFields, err := parseModelFields(baseModel.GoDocument, obj.Name, baseModel)
if err != nil {
return err
}
structFields[obj.Name] = make(map[string]modelField)
for _, f := range objFields {
structFields[obj.Name][f.FieldName] = f
}
}
// Append the struct fields to be used for template execution.
if tmplData == nil {
tmplData = make(map[string]interface{})
}
tmplData["StructFields"] = structFields
// Execute template and parse code to be used to compare against modelFile.
tmplObjs, err := loadTemplateObjects(log, baseModel, templateDir, "model_crud.tmpl", tmplData)
if err != nil {
return err
}
// Store the current code as a string to produce a diff.
curCode := crudDoc.String()
objHeaders := []*goparse.GoObject{}
for _, obj := range tmplObjs {
if obj.Type == goparse.GoObjectType_Comment || obj.Type == goparse.GoObjectType_LineBreak {
objHeaders = append(objHeaders, obj)
continue
}
if crudDoc.HasType(obj.Name, obj.Type) {
cur := crudDoc.Objects().Get(obj.Name, obj.Type)
newObjs := []*goparse.GoObject{}
if len(objHeaders) > 0 {
// Remove any comments and linebreaks before the existing object so updates can be added.
removeObjs := []*goparse.GoObject{}
for idx := cur.Index - 1; idx > 0; idx-- {
prevObj := crudDoc.Objects().List()[idx]
if prevObj.Type == goparse.GoObjectType_Comment || prevObj.Type == goparse.GoObjectType_LineBreak {
removeObjs = append(removeObjs, prevObj)
} else {
break
}
}
if len(removeObjs) > 0 {
err := crudDoc.Objects().Remove(removeObjs...)
if err != nil {
err = errors.WithMessagef(err, "Failed to update object %s %s for %s", obj.Type, obj.Name, baseModel.Name)
return err
}
// Make sure the current index is correct.
cur = crudDoc.Objects().Get(obj.Name, obj.Type)
}
// Append comments and line breaks before adding the object
for _, c := range objHeaders {
newObjs = append(newObjs, c)
}
}
newObjs = append(newObjs, obj)
// Do the object replacement.
err := crudDoc.Objects().Replace(cur, newObjs...)
if err != nil {
err = errors.WithMessagef(err, "Failed to update object %s %s for %s", obj.Type, obj.Name, baseModel.Name)
return err
}
} else {
// Append comments and line breaks before adding the object
for _, c := range objHeaders {
err := crudDoc.Objects().Add(c)
if err != nil {
err = errors.WithMessagef(err, "Failed to add object %s %s for %s", c.Type, c.Name, baseModel.Name)
return err
}
}
err := crudDoc.Objects().Add(obj)
if err != nil {
err = errors.WithMessagef(err, "Failed to add object %s %s for %s", obj.Type, obj.Name, baseModel.Name)
return err
}
}
objHeaders = []*goparse.GoObject{}
}
/*
// Set some flags to determine additional imports and need to be added.
var hasEnum bool
var hasPq bool
for _, f := range crudModel.Fields {
if f.DbColumn != nil && f.DbColumn.IsEnum {
hasEnum = true
}
if strings.HasPrefix(strings.Trim(f.FieldType, "*"), "pq.") {
hasPq = true
}
}
reqImports := []string{}
if hasEnum {
reqImports = append(reqImports, "database/sql/driver")
reqImports = append(reqImports, "gopkg.in/go-playground/validator.v9")
reqImports = append(reqImports, "github.com/pkg/errors")
}
if hasPq {
reqImports = append(reqImports, "github.com/lib/pq")
}
for _, in := range reqImports {
err := model.AddImport(goparse.GoImport{Name: in})
if err != nil {
err = errors.WithMessagef(err, "Failed to add import %s for %s", in, crudModel.Name)
return err
}
}
*/
// Produce a diff after the updates have been applied.
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(curCode, crudDoc.String(), true)
fmt.Println(dmp.DiffPrettyText(diffs))
return nil
}