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(¶ms.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 }