github.com/voedger/voedger@v0.0.0-20240520144910-273e84102129/cmd/vpm/orm.go (about)

     1  /*
     2   * Copyright (c) 2024-present unTill Pro, Ltd.
     3   * @author Alisher Nurmanov
     4   */
     5  
     6  package main
     7  
     8  import (
     9  	"bytes"
    10  	"embed"
    11  	"fmt"
    12  	"go/format"
    13  	"io/fs"
    14  	"os"
    15  	"path/filepath"
    16  	"slices"
    17  	"strings"
    18  	"text/template"
    19  
    20  	"github.com/spf13/cobra"
    21  
    22  	"github.com/voedger/voedger/pkg/appdef"
    23  	"github.com/voedger/voedger/pkg/compile"
    24  	"github.com/voedger/voedger/pkg/sys"
    25  	coreutils "github.com/voedger/voedger/pkg/utils"
    26  )
    27  
    28  //go:embed ormtemplates/*
    29  var ormTemplatesFS embed.FS
    30  var reservedWords = []string{"type"}
    31  
    32  func newOrmCmd(params *vpmParams) *cobra.Command {
    33  	cmd := &cobra.Command{
    34  		Use:   "orm",
    35  		Short: "generate orm for package",
    36  		RunE: func(cmd *cobra.Command, args []string) (err error) {
    37  			compileRes, err := compile.Compile(params.Dir)
    38  			if err != nil {
    39  				return err
    40  			}
    41  			return generateOrm(compileRes, params)
    42  		},
    43  	}
    44  	cmd.Flags().StringVarP(&params.HeaderFile, "header-file", "", "", "path to file to insert as a header to generated files")
    45  	return cmd
    46  }
    47  
    48  // generateOrm generates ORM from the given working directory
    49  func generateOrm(compileRes *compile.Result, params *vpmParams) error {
    50  	dir, err := createOrmDir(params.Dir)
    51  	if err != nil {
    52  		return err
    53  	}
    54  
    55  	headerContent, err := getHeaderFileContent(params.HeaderFile)
    56  	if err != nil {
    57  		return err
    58  	}
    59  
    60  	iTypeObjs, pkgInfos, currentPkgLocalName := getPkgAppDefObjs(compileRes.ModulePath, compileRes.AppDef, headerContent)
    61  	pkgData := getOrmData(currentPkgLocalName, pkgInfos, iTypeObjs)
    62  	if err := generateOrmFiles(pkgData, dir); err != nil {
    63  		return err
    64  	}
    65  	// update dependencies if go.mod file exists
    66  	if err := execGoModTidy(dir); err != nil {
    67  		return err
    68  	}
    69  	return nil
    70  }
    71  
    72  // getPkgAppDefObjs gathers objects from the current package
    73  // and returns a list of objects, a map of package local names to its info and the current package local name
    74  func getPkgAppDefObjs(packagePath string, appDef appdef.IAppDef, headerContent string) (iTypeObjs []appdef.IType, pkgInfos map[string]ormPackageInfo, currentPkgLocalName string) {
    75  	uniqueObjects := make([]string, 0)
    76  	iTypeObjs = make([]appdef.IType, 0)        // list of package objects
    77  	pkgInfos = make(map[string]ormPackageInfo) // mapping of package local names to its info
    78  	// sys package is implicitly added to the list of packages,
    79  	// so we need to add it manually
    80  	currentPkgLocalName = appdef.SysPackage
    81  	pkgInfos[appdef.SysPackage] = ormPackageInfo{
    82  		Name:              appdef.SysPackage,
    83  		FullPath:          sys.PackagePath,
    84  		HeaderFileContent: headerContent,
    85  	}
    86  	appDef.Packages(func(localName, fullPath string) {
    87  		if fullPath == packagePath {
    88  			currentPkgLocalName = localName
    89  		}
    90  		pkgInfos[localName] = ormPackageInfo{
    91  			Name:              localName,
    92  			FullPath:          fullPath,
    93  			HeaderFileContent: headerContent,
    94  		}
    95  	})
    96  
    97  	collectITypeObjs := func(iTypeObj appdef.IType) {
    98  		// skip abstract types
    99  		if iAbstract, ok := iTypeObj.(appdef.IWithAbstract); ok {
   100  			if iAbstract.Abstract() {
   101  				return
   102  			}
   103  		}
   104  		qName := iTypeObj.QName()
   105  		// skip types from other packages
   106  		if qName.Pkg() != currentPkgLocalName {
   107  			return
   108  		}
   109  		if !slices.Contains(uniqueObjects, qName.String()) {
   110  			iTypeObjs = append(iTypeObjs, iTypeObj)
   111  			uniqueObjects = append(uniqueObjects, qName.String())
   112  		}
   113  	}
   114  
   115  	// gather objects from the current package
   116  	appDef.Types(func(iTypeObj appdef.IType) {
   117  		if workspace, ok := iTypeObj.(appdef.IWorkspace); ok {
   118  			workspace.Types(collectITypeObjs)
   119  		}
   120  	})
   121  	return
   122  }
   123  
   124  func generateOrmFiles(pkgData map[ormPackageInfo][]interface{}, dir string) error {
   125  	ormFiles := make([]string, 0, len(pkgData)+1)
   126  	for pkgInfo, pkgItems := range pkgData {
   127  		ormPkgData := ormPackage{
   128  			ormPackageInfo: pkgInfo,
   129  			Items:          pkgItems,
   130  		}
   131  		ormFilePath, err := generateOrmFile(pkgInfo.Name, ormPkgData, dir)
   132  		if err != nil {
   133  			return fmt.Errorf(errInGeneratingOrmFileFormat, ormFilePath, err)
   134  		}
   135  		ormFiles = append(ormFiles, ormFilePath)
   136  	}
   137  
   138  	sysFilePath := filepath.Join(dir, "types.go")
   139  	ormFiles = append(ormFiles, sysFilePath)
   140  	if err := os.WriteFile(sysFilePath, []byte(sysContent), coreutils.FileMode_rw_rw_rw_); err != nil {
   141  		return fmt.Errorf(errInGeneratingOrmFileFormat, sysFilePath, err)
   142  	}
   143  
   144  	return formatOrmFiles(ormFiles)
   145  }
   146  
   147  func formatOrmFiles(ormFiles []string) error {
   148  	for _, ormFile := range ormFiles {
   149  		ormFileContent, err := os.ReadFile(ormFile)
   150  		if err != nil {
   151  			return err
   152  		}
   153  
   154  		formattedContent, err := format.Source(ormFileContent)
   155  		if err != nil {
   156  			return err
   157  		}
   158  
   159  		if err := os.WriteFile(ormFile, formattedContent, coreutils.FileMode_rw_rw_rw_); err != nil {
   160  			return err
   161  		}
   162  	}
   163  	return nil
   164  }
   165  
   166  func generateOrmFile(localName string, ormPkgData ormPackage, dir string) (filePath string, err error) {
   167  	filePath = filepath.Join(dir, fmt.Sprintf("package_%s.go", localName))
   168  	ormFileContent, err := fillInTemplate(ormPkgData)
   169  	if err != nil {
   170  		return filePath, err
   171  	}
   172  
   173  	if err := os.WriteFile(filePath, ormFileContent, coreutils.FileMode_rw_rw_rw_); err != nil {
   174  		return filePath, err
   175  	}
   176  	return filePath, nil
   177  }
   178  
   179  func getOrmData(localName string, pkgInfos map[string]ormPackageInfo, iTypeObjs []appdef.IType) (pkgData map[ormPackageInfo][]interface{}) {
   180  	pkgData = make(map[ormPackageInfo][]interface{})
   181  	uniquePkgQNames := make(map[ormPackageInfo][]string)
   182  	for _, obj := range iTypeObjs {
   183  		processITypeObj(localName, pkgInfos, pkgData, uniquePkgQNames, obj)
   184  	}
   185  	return
   186  }
   187  
   188  func newPackageItem(defaultLocalName string, pkgInfos map[string]ormPackageInfo, obj interface{}) ormPackageItem {
   189  	name := getName(obj)
   190  	qName := obj.(appdef.IType).QName()
   191  	localName := defaultLocalName
   192  	if obj != nil {
   193  		localName = qName.Pkg()
   194  	}
   195  	pkgInfo := pkgInfos[localName]
   196  	return ormPackageItem{
   197  		Package:   pkgInfo,
   198  		QName:     qName.String(),
   199  		TypeQName: fmt.Sprintf("%s.%s", pkgInfo.FullPath, name),
   200  		Name:      name,
   201  		Type:      getObjType(obj),
   202  	}
   203  }
   204  
   205  func newFieldItem(tableData ormTableItem, field appdef.IField) ormField {
   206  	name := normalizeName(field.Name())
   207  	return ormField{
   208  		Table:         tableData,
   209  		Type:          getFieldType(field),
   210  		Name:          normalizeName(field.Name()),
   211  		GetMethodName: fmt.Sprintf("Get_%s", strings.ToLower(name)),
   212  		SetMethodName: fmt.Sprintf("Set_%s", strings.ToLower(name)),
   213  	}
   214  }
   215  
   216  func processITypeObj(localName string, pkgInfos map[string]ormPackageInfo, pkgData map[ormPackageInfo][]interface{}, uniquePkgQNames map[ormPackageInfo][]string, obj appdef.IType) (newItem interface{}) {
   217  	if obj == nil {
   218  		return nil
   219  	}
   220  
   221  	pkgItem := newPackageItem(localName, pkgInfos, obj)
   222  	if pkgItem.Type == unknownType {
   223  		return nil
   224  	}
   225  
   226  	switch t := obj.(type) {
   227  	case appdef.ICDoc, appdef.IWDoc, appdef.IView, appdef.IODoc, appdef.IObject:
   228  		tableData := ormTableItem{
   229  			ormPackageItem: pkgItem,
   230  			Fields:         make([]ormField, 0),
   231  		}
   232  
   233  		iView, isView := t.(appdef.IView)
   234  		if isView {
   235  			for _, key := range iView.Key().Fields() {
   236  				fieldItem := newFieldItem(tableData, key)
   237  				if fieldItem.Type == unknownType {
   238  					continue
   239  				}
   240  				tableData.Keys = append(tableData.Keys, fieldItem)
   241  			}
   242  		}
   243  		// fetching fields
   244  		for _, field := range t.(appdef.IFields).Fields() {
   245  			fieldItem := newFieldItem(tableData, field)
   246  			if fieldItem.Type == unknownType {
   247  				continue
   248  			}
   249  
   250  			isKey := false
   251  			for _, key := range tableData.Keys {
   252  				if key.Name == fieldItem.Name {
   253  					isKey = true
   254  					break
   255  				}
   256  			}
   257  			if !isKey {
   258  				tableData.Fields = append(tableData.Fields, fieldItem)
   259  			}
   260  		}
   261  		newItem = tableData
   262  	case appdef.ICommand, appdef.IQuery:
   263  		var resultFields []ormField
   264  		argumentObj := processITypeObj(localName, pkgInfos, pkgData, uniquePkgQNames, t.(appdef.IFunction).Param())
   265  
   266  		var unloggedArgumentObj interface{}
   267  		if iCommand, ok := t.(appdef.ICommand); ok {
   268  			unloggedArgumentObj = processITypeObj(localName, pkgInfos, pkgData, uniquePkgQNames, iCommand.UnloggedParam())
   269  		}
   270  		if resultObj := processITypeObj(localName, pkgInfos, pkgData, uniquePkgQNames, t.(appdef.IFunction).Result()); resultObj != nil {
   271  			if tableData, ok := resultObj.(ormTableItem); ok {
   272  				resultFields = tableData.Fields
   273  			}
   274  		}
   275  
   276  		commandItem := ormCommand{
   277  			ormPackageItem:         pkgItem,
   278  			ArgumentObject:         argumentObj,
   279  			ResultObjectFields:     resultFields,
   280  			UnloggedArgumentObject: unloggedArgumentObj,
   281  		}
   282  		newItem = commandItem
   283  	default:
   284  		typeKind := t.Kind()
   285  		if typeKind == appdef.TypeKind_Object {
   286  			return processITypeObj(localName, pkgInfos, pkgData, uniquePkgQNames, t.(appdef.IObject))
   287  		}
   288  		newItem = pkgItem
   289  	}
   290  	// add new package item to the package data
   291  	if !slices.Contains(uniquePkgQNames[pkgItem.Package], getQName(newItem)) {
   292  		pkgData[pkgItem.Package] = append(pkgData[pkgItem.Package], newItem)
   293  		uniquePkgQNames[pkgItem.Package] = append(uniquePkgQNames[pkgItem.Package], getQName(newItem))
   294  	}
   295  	return
   296  }
   297  
   298  func fillInTemplate(ormPkgData ormPackage) ([]byte, error) {
   299  	ormTemplates, err := fs.Sub(ormTemplatesFS, "ormtemplates")
   300  	if err != nil {
   301  		return nil, fmt.Errorf("failed to read templates directory: %w", err)
   302  	}
   303  	t, err := template.New("package").Funcs(template.FuncMap{
   304  		"capitalize": func(s string) string {
   305  			if len(s) == 0 {
   306  				return s
   307  			}
   308  			return strings.ToUpper(s[:1]) + s[1:]
   309  		},
   310  		"lower": strings.ToLower,
   311  	}).ParseFS(ormTemplates, "*")
   312  	if err != nil {
   313  		return nil, fmt.Errorf("failed to parse template: %w", err)
   314  	}
   315  
   316  	var filledTemplate bytes.Buffer
   317  	if err := t.ExecuteTemplate(&filledTemplate, "package", ormPkgData); err != nil {
   318  		return nil, fmt.Errorf("failed to fill template: %w", err)
   319  	}
   320  
   321  	return filledTemplate.Bytes(), nil
   322  }
   323  
   324  func getHeaderFileContent(headerFilePath string) (string, error) {
   325  	if headerFilePath == "" {
   326  		return defaultOrmFilesHeaderComment, nil
   327  	}
   328  
   329  	headerFileContent, err := os.ReadFile(headerFilePath)
   330  	if err != nil {
   331  		return "", err
   332  	}
   333  
   334  	return string(headerFileContent), nil
   335  }
   336  
   337  func createOrmDir(dir string) (string, error) {
   338  	ormDirPath := filepath.Join(dir, wasmDirName, ormDirName)
   339  	exists, err := coreutils.Exists(ormDirPath)
   340  	if err != nil {
   341  		// notest
   342  		return "", err
   343  	}
   344  	if exists {
   345  		if err := os.RemoveAll(ormDirPath); err != nil {
   346  			return "", err
   347  		}
   348  	}
   349  	return ormDirPath, os.MkdirAll(ormDirPath, coreutils.FileMode_rwxrwxrwx)
   350  }
   351  
   352  func normalizeName(name string) (newName string) {
   353  	newName = strings.ReplaceAll(name, ".", "_")
   354  	if slices.Contains(reservedWords, strings.ToLower(newName)) {
   355  		newName += "_"
   356  	}
   357  	return
   358  }
   359  
   360  func getName(obj interface{}) string {
   361  	if obj == nil {
   362  		return ""
   363  	}
   364  	return normalizeName(obj.(appdef.IType).QName().Entity())
   365  }
   366  
   367  func getObjType(obj interface{}) string {
   368  	switch t := obj.(type) {
   369  	case appdef.IODoc:
   370  		return "ODoc"
   371  	case appdef.ICDoc:
   372  		if t.Singleton() {
   373  			return "CSingleton"
   374  		}
   375  		return "CDoc"
   376  	case appdef.IWDoc:
   377  		if t.Singleton() {
   378  			return "WSingleton"
   379  		}
   380  		return "WDoc"
   381  	case appdef.IView:
   382  		return "View"
   383  	case appdef.ICommand:
   384  		return "Command"
   385  	case appdef.IQuery:
   386  		return "Query"
   387  	case appdef.IObject:
   388  		return getTypeKind(t.Kind())
   389  	case appdef.IType:
   390  		return getTypeKind(t.Kind())
   391  	default:
   392  		return unknownType
   393  	}
   394  }
   395  
   396  func getTypeKind(typeKind appdef.TypeKind) string {
   397  	switch typeKind {
   398  	case appdef.TypeKind_Object:
   399  		return "Type"
   400  	case appdef.TypeKind_CDoc:
   401  		return "CDoc"
   402  	case appdef.TypeKind_WDoc:
   403  		return "WDoc"
   404  	case appdef.TypeKind_ODoc:
   405  		return "ODoc"
   406  	default:
   407  		return unknownType
   408  	}
   409  }
   410  
   411  func getFieldType(field appdef.IField) string {
   412  	switch field.DataKind() {
   413  	case appdef.DataKind_bool:
   414  		return "bool"
   415  	case appdef.DataKind_int32:
   416  		return "int32"
   417  	case appdef.DataKind_int64:
   418  		return "int64"
   419  	case appdef.DataKind_float32:
   420  		return "float32"
   421  	case appdef.DataKind_float64:
   422  		return "float64"
   423  	case appdef.DataKind_bytes:
   424  		return "Bytes"
   425  	case appdef.DataKind_string:
   426  		return "string"
   427  	case appdef.DataKind_RecordID:
   428  		return "Ref"
   429  	default:
   430  		return unknownType
   431  	}
   432  }