github.com/mailru/activerecord@v1.12.2/internal/app/argen.go (about) 1 // Пакет app - основной пакет приложения. 2 // 3 // Приложение проходит несколько этапов парсинг, проверку и генерацию 4 // При инициализации указывается путь где находится описание 5 // и путь где сохраняются результирующие структуры. 6 package app 7 8 import ( 9 "context" 10 "errors" 11 "fmt" 12 "io/fs" 13 "log" 14 "os" 15 "path/filepath" 16 "regexp" 17 "strings" 18 "sync" 19 20 "github.com/mailru/activerecord/internal/pkg/arerror" 21 "github.com/mailru/activerecord/internal/pkg/checker" 22 "github.com/mailru/activerecord/internal/pkg/ds" 23 "github.com/mailru/activerecord/internal/pkg/generator" 24 "github.com/mailru/activerecord/internal/pkg/parser" 25 ) 26 27 // Структура приложения 28 // src и dst - исходная и конечная папки используетмые для генерации репозиториеа 29 // dstFixture - конечная папка для генерации тестовых фикстур для сгенерированных репозиториев 30 // srcEntry, dstEntry - используется для хранения содержимого 31 // соответствующих папок на момент запуска приложения 32 // packagesParsed, packagesLock - мапка с обработанными пакетами 33 // и мьютекс для блокировки при их добавлении 34 // packagesLinked - используется для хранения информации о ссылках 35 // appInfo - включает информацию о самом пакете, эта информация используется 36 // при генерации конечных фалов. 37 // modName - имя модуля используемое для построения путей import-а 38 type ArGen struct { 39 ctx context.Context 40 src, dst string 41 srcEntry, dstEntry []fs.DirEntry 42 packagesParsed map[string]*ds.RecordPackage 43 packagesLock sync.Mutex 44 packagesLinked map[string]string 45 appInfo *ds.AppInfo 46 modName string 47 fileToRemove map[string]bool 48 dstFixture string 49 } 50 51 // Пропускает этап генерации сторов фикстур, 52 // если не задан путь к папке 53 func (a *ArGen) skipGenerateFixture() bool { 54 return len(a.dstFixture) == 0 55 } 56 57 // Инициализация приложения 58 // информацию по параметрам см. в описании структуры ArGen 59 func Init(ctx context.Context, appInfo *ds.AppInfo, srcDir, dstDir, fixtureDir, modName string) (*ArGen, error) { 60 argen := ArGen{ 61 ctx: ctx, 62 src: srcDir, 63 dst: dstDir, 64 dstFixture: fixtureDir, 65 srcEntry: []fs.DirEntry{}, 66 dstEntry: []fs.DirEntry{}, 67 packagesParsed: map[string]*ds.RecordPackage{}, 68 packagesLock: sync.Mutex{}, 69 packagesLinked: map[string]string{}, 70 appInfo: appInfo, 71 modName: modName, 72 fileToRemove: map[string]bool{}, 73 } 74 75 // Подготавливаем информацию из src и dst директорий 76 err := argen.prepareDir() 77 if err != nil { 78 return nil, fmt.Errorf("error prepare dirs: %w", err) 79 } 80 81 return &argen, nil 82 } 83 84 // Регулярное выражения для проверки названий пакетов 85 // сейчас поддерживаются пакеты состоящие исключительно из 86 // маленьких латинских символов, длинной не более 20 87 var rxPkgName = regexp.MustCompile(`^[a-z]{1,20}$`) 88 89 // Функция для добаление записи об очередном обработанном файле 90 func (a *ArGen) addRecordPackage(pkgName string) (*ds.RecordPackage, error) { 91 a.packagesLock.Lock() 92 defer a.packagesLock.Unlock() 93 94 // Хорошее имя пакета должно быть коротким и чистым 95 // Состоит только из букв в нижнем регистре, без подчеркиваний 96 // Обычно это простые существительные в единственном числе 97 if !rxPkgName.MatchString(pkgName) { 98 return nil, &arerror.ErrParseGenDecl{Name: pkgName, Err: arerror.ErrBadPkgName} 99 } 100 101 // проверка на то, что такого пакета ранее не было 102 if _, ex := a.packagesParsed[pkgName]; ex { 103 return nil, &arerror.ErrParseGenDecl{Name: pkgName, Err: arerror.ErrRedefined} 104 } 105 106 rp := ds.NewRecordPackage() 107 a.packagesParsed[pkgName] = rp 108 109 return a.packagesParsed[pkgName], nil 110 } 111 112 // Подготовка к проверке всех обработанных пакетов-деклараций 113 // Обогащаем информацию по пакетам путями для импорта и 114 // строим обратный индекс от имен структур к именам файлов 115 func (a *ArGen) prepareCheck() error { 116 for key, pkg := range a.packagesParsed { 117 pkg.Namespace.ModuleName = filepath.Join(a.modName, a.dst, pkg.Namespace.PackageName) 118 a.packagesLinked[pkg.Namespace.PackageName] = key 119 a.packagesParsed[key] = pkg 120 } 121 122 // Сбор по подготовка информации по существующим файлам в результирующей директории 123 exists, err := a.getExists() 124 if err != nil { 125 return fmt.Errorf("can't get exists files: %s", err) 126 } 127 128 // Строим мапку по существующим файлам для определения "лишних" фалов в 129 // результирующей директории 130 for _, file := range exists { 131 a.fileToRemove[file] = true 132 } 133 134 return nil 135 } 136 137 func (a *ArGen) prepareGenerate(cl *ds.RecordPackage) (map[string]ds.RecordPackage, error) { 138 linkObjects := map[string]ds.RecordPackage{} 139 for _, fo := range cl.FieldsObjectMap { 140 linkObjects[fo.ObjectName] = *a.packagesParsed[a.packagesLinked[fo.ObjectName]] 141 142 _, err := cl.FindOrAddImport(linkObjects[fo.ObjectName].Namespace.ModuleName, linkObjects[fo.ObjectName].Namespace.PackageName) 143 if err != nil { 144 return nil, fmt.Errorf("error process `%s` linkObject for package `%s`: %s", fo.ObjectName, cl.Namespace.PublicName, err) 145 } 146 } 147 148 // Подготавливаем информацию по типам индексов 149 for indexNum := range cl.Indexes { 150 if len(cl.Indexes[indexNum].Fields) > 1 { 151 cl.Indexes[indexNum].Type = cl.Indexes[indexNum].Name + "IndexType" 152 } else { 153 cl.Indexes[indexNum].Type = string(cl.Fields[cl.Indexes[indexNum].Fields[0]].Format) 154 } 155 } 156 157 return linkObjects, nil 158 } 159 160 func (a *ArGen) prepareFixtureGenerate(cl *ds.RecordPackage, name string) error { 161 for _, fo := range cl.FieldsObjectMap { 162 linkObjectPackage := *a.packagesParsed[a.packagesLinked[fo.ObjectName]] 163 164 _, err := cl.FindOrAddImport(linkObjectPackage.Namespace.ModuleName, linkObjectPackage.Namespace.PackageName) 165 if err != nil { 166 return fmt.Errorf("error process `%s` linkObject for package `%s`: %s", fo.ObjectName, cl.Namespace.PublicName, err) 167 } 168 } 169 170 recordPackage := *a.packagesParsed[a.packagesLinked[name]] 171 172 _, err := cl.FindOrAddImport(recordPackage.Namespace.ModuleName, name) 173 if err != nil { 174 return fmt.Errorf("error process `%s` add import declaration for package `%s`: %s", name, cl.Namespace.PublicName, err) 175 } 176 177 return nil 178 } 179 180 func (a *ArGen) saveGenerateResult(name, dst string, genRes []generator.GenerateFile) error { 181 for _, gen := range genRes { 182 dirPkg := filepath.Join(dst, gen.Dir) 183 dstFileName := filepath.Join(dirPkg, gen.Name) 184 185 // Сохранение результата генерации в файл 186 log.Printf("Write package `%s` (%s) into file `%s`", name, dstFileName, dstFileName) 187 188 if err := writeToFile(dirPkg, dstFileName, gen.Data); err != nil { 189 return &arerror.ErrGeneratorFile{Name: name, Backend: gen.Backend, Filename: dstFileName, Err: err} 190 } 191 192 // Удаляем из "лишних" фалов то, что перегенерировали 193 if _, ex := a.fileToRemove[dstFileName]; ex { 194 log.Printf("Replace file: %s", dstFileName) 195 delete(a.fileToRemove, dstFileName) 196 } else { 197 log.Printf("Create file: %s", dstFileName) 198 } 199 200 // Удаляем из лишних все директории сгенерированных пакетов 201 if _, ex := a.fileToRemove[dirPkg]; ex { 202 log.Printf("Replace dir: %s", dirPkg) 203 delete(a.fileToRemove, dirPkg) 204 } 205 } 206 207 return nil 208 } 209 210 // Процесс генерации пакетов по подготовленным данным 211 func (a *ArGen) generate() error { 212 // Запускаем цикл с проходом по всем полученным файлам для генерации 213 // результирующих пакетов 214 for name, cl := range a.packagesParsed { 215 // Подготовка информации по ссылкам на другие пакеты 216 linkObjects, err := a.prepareGenerate(cl) 217 if err != nil { 218 return fmt.Errorf("prepare generate error: %s", err) 219 } 220 221 // Процесс генерации 222 genRes, genErr := generator.Generate(a.appInfo.String(), *cl, linkObjects) 223 if genErr != nil { 224 return fmt.Errorf("generate error: %s", genErr) 225 } 226 227 if err := a.saveGenerateResult(name, a.dst, genRes); err != nil { 228 return fmt.Errorf("error save result: %w", err) 229 } 230 } 231 232 metadata, err := a.prepareMetaData() 233 if err != nil { 234 return fmt.Errorf("prepare metadata generate error: %s", err) 235 } 236 237 if len(metadata.Namespaces) > 0 { 238 genRes, genErr := generator.GenerateMeta(metadata) 239 if genErr != nil { 240 return fmt.Errorf("generate meta error: %s", genErr) 241 } 242 243 if genErr := a.saveGenerateResult("meta", a.dst, genRes); genErr != nil { 244 return fmt.Errorf("error save meta result: %w", err) 245 } 246 } 247 248 if a.skipGenerateFixture() { 249 return nil 250 } 251 252 // Генерация пакета со сторами фикстур для тестов 253 err = a.prepareFixturesStorage() 254 if err != nil { 255 return fmt.Errorf("prepare fixture store error: %s", err) 256 } 257 258 fixtureDir, fxtPkg := filepath.Split(a.dstFixture) 259 260 for name, cl := range a.packagesParsed { 261 // Подготовка информации по ссылкам на другие пакеты 262 err := a.prepareFixtureGenerate(cl, name) 263 if err != nil { 264 return fmt.Errorf("prepare generate %s fixture store error: %w", name, err) 265 } 266 267 // Процесс генерации 268 genRes, genErr := generator.GenerateFixture(a.appInfo.String(), *cl, name, fxtPkg) 269 if genErr != nil { 270 return fmt.Errorf("generate %s fixture store error: %w", name, genErr) 271 } 272 273 if err := a.saveGenerateResult(name, fixtureDir, genRes); err != nil { 274 return fmt.Errorf("error save generated %s fixture result: %w", name, err) 275 } 276 } 277 278 namespaces := map[string][]*ds.RecordPackage{} 279 280 for _, cl := range a.packagesParsed { 281 for _, backend := range cl.Backends { 282 namespaces[backend] = append(namespaces[backend], cl) 283 } 284 } 285 286 genRes, genErr := generator.GenerateFixtureMeta(namespaces, a.appInfo.String(), fxtPkg) 287 if genErr != nil { 288 return fmt.Errorf("generate fixture meta error: %s", genErr) 289 } 290 291 if err := a.saveGenerateResult("fixture_meta", fixtureDir, genRes); err != nil { 292 return fmt.Errorf("error save fixture meta result: %w", err) 293 } 294 295 return nil 296 } 297 298 // Основная функция запускающая конвеер на выполнение 299 // всех этапов генерации 300 // - парсинг 301 // - обогащение перед проверкой 302 // - Проверка 303 // - сбор существующих файлов 304 // - генерация 305 // - очистка "лишних" файлов 306 func (a *ArGen) Run() error { 307 // парсим декларацию сущностей 308 if err := a.parse(); err != nil { 309 return err 310 } 311 312 // Подготавливаем данные для проверки 313 if err := a.prepareCheck(); err != nil { 314 return fmt.Errorf("can't prepare files: %s", err) 315 } 316 317 // Проверка информации полученной из декларации на консистентность и валидность 318 checkErr := checker.Check(a.packagesParsed, a.packagesLinked) 319 if checkErr != nil { 320 return fmt.Errorf("error check repository after parse: %s", checkErr) 321 } 322 323 if err := a.generate(); err != nil { 324 return fmt.Errorf("error generate: %w", err) 325 } 326 327 // Очищаем лишние файлы 328 for name := range a.fileToRemove { 329 log.Printf("Drop file `%s`\n", name) 330 os.Remove(name) 331 } 332 333 return nil 334 } 335 336 // Создание директории для пакета и запись пакета на диск 337 func writeToFile(dirPkg string, dstFileName string, data []byte) error { 338 if !strings.HasPrefix(dstFileName, dirPkg) { 339 return fmt.Errorf("dstFileName must be into dstDir") 340 } 341 342 if _, err := os.Stat(dirPkg); err != nil { 343 if errors.Is(err, os.ErrNotExist) { 344 if err = os.Mkdir(dirPkg, 0700); err != nil { 345 return fmt.Errorf("error create dir while save to file: %w", err) 346 } 347 } else { 348 return fmt.Errorf("save generated package to file error: %w", err) 349 } 350 } 351 352 dstFile, err := os.Create(dstFileName) //nolint:gosec 353 if err != nil { 354 return fmt.Errorf("error create file: %w", err) 355 } 356 357 _, err = dstFile.Write(data) 358 if err != nil { 359 return fmt.Errorf("error write to file: %w", err) 360 } 361 362 dstFile.Close() 363 364 return nil 365 } 366 367 // Сункция обрабатывает все декларации в папке src 368 // результат парсинга складывает в packagesParsed 369 func (a *ArGen) parse() error { 370 for _, srcFile := range a.srcEntry { 371 if !srcFile.Type().IsRegular() { 372 return fmt.Errorf("error declaration file `%s`. File in model declaration dir must be regular", srcFile.Name()) 373 } 374 375 srcFileName := filepath.Join(a.src, srcFile.Name()) 376 source := srcFile.Name() 377 378 // Создаём новую запись для очередного файла 379 // при создании проверяются дубликаты деклараций 380 rc, err := a.addRecordPackage(source[:len(source)-3]) 381 if err != nil { 382 return fmt.Errorf("error model(%s) parse: %s", srcFileName, err) 383 } 384 385 rc.Namespace.ModuleName = a.modName 386 387 // Запускаем процесс парсинга 388 if err := parser.Parse(srcFileName, rc); err != nil { 389 return fmt.Errorf("error parse declaration: %w", err) 390 } 391 } 392 393 return nil 394 } 395 396 // Регулярное выражение для проверки пути 397 // Путь участвует в построении пути для импорта, по этому 398 // нельзя использовать точки, но можно относительно текущей диры 399 var rxPathValidator = regexp.MustCompile(`^[^\.]`) 400 401 // Подготовка рабочих каталогов, чтение деклараций 402 // Если каталог для генерации существует то проверяется наличие файла .argen 403 // что бы случайно не удалить какие то файлы после генерации 404 // Если каталога нет, то он создаётся с файлом .argen для будущих перегенераций 405 func (a *ArGen) prepareDir() error { 406 var err error 407 408 if !rxPathValidator.MatchString(a.src) { 409 return fmt.Errorf("invalid path repository declaration") 410 } 411 412 if !rxPathValidator.MatchString(a.dst) { 413 return fmt.Errorf("invaliv path repository generation") 414 } 415 416 a.srcEntry, err = os.ReadDir(a.src) 417 if err != nil { 418 return fmt.Errorf("error open dir `%s` with repository declaration: %w", a.src, err) 419 } 420 421 // Проверка существования каталога для генерации, если нет то создаём 422 a.dstEntry, err = os.ReadDir(a.dst) 423 if err != nil { 424 if !os.IsNotExist(err) { 425 return fmt.Errorf("error open dir `%s` for repository generation: %w", a.dst, err) 426 } 427 428 err := os.Mkdir(a.dst, 0750) 429 if err != nil { 430 return fmt.Errorf("error create dir `%s` for repository generation: %w", a.dst, err) 431 } 432 433 if err := os.WriteFile(filepath.Join(a.dst, ".argen"), []byte("DO NOT DELETE THIS FILE"), 0600); err != nil { 434 return fmt.Errorf("error create spec file `.argen` for repository generation: %w", err) 435 } 436 437 a.dstEntry = []fs.DirEntry{} 438 } else { 439 file, err := os.Open(filepath.Join(a.dst, ".argen")) 440 if err != nil { 441 return fmt.Errorf("destination directory not empty and hasn't .argen special file") 442 } 443 444 file.Close() 445 } 446 447 return nil 448 } 449 450 // Подготавливает файлы стораджей для сгенерированных сторов фикстур 451 func (a *ArGen) prepareFixturesStorage() error { 452 var err error 453 454 if !rxPathValidator.MatchString(a.dstFixture) { 455 return fmt.Errorf("invaliv path for fixture generation") 456 } 457 458 storePath := filepath.Join(a.dstFixture, "data") 459 // Проверка существования папки для хранилища фикстур, если нет то создаём 460 _, err = os.ReadDir(storePath) 461 if err != nil { 462 if !os.IsNotExist(err) { 463 return fmt.Errorf("error open dir `%s` for fixture storage: %w", storePath, err) 464 } 465 466 err = os.MkdirAll(storePath, 0750) 467 if err != nil { 468 return fmt.Errorf("error create dir `%s` for fixture storage: %w", storePath, err) 469 } 470 } 471 // Для всех генерируемых сущностей создаем файлы для хранения фикстур, если они еще не существуют 472 for name, p := range a.packagesParsed { 473 typeNames := []string{""} 474 // Если не процедура, создаем файлы для фикстур операций модификации данных 475 if len(p.ProcFieldsMap) == 0 { 476 typeNames = []string{"", "_update", "_insert_replace"} 477 } 478 479 for _, fixtureType := range typeNames { 480 storagePath := filepath.Join(storePath, name+fixtureType+".yaml") 481 482 if _, err = os.Stat(storagePath); os.IsNotExist(err) { 483 if err = os.WriteFile(storagePath, nil, fs.ModePerm); err != nil { 484 return fmt.Errorf("error create storage file for %s fixture storage: %w", name, err) 485 } 486 } 487 488 if err != nil { 489 return fmt.Errorf("error check file for %s fixture storage: %w", name, err) 490 } 491 } 492 } 493 494 return nil 495 } 496 497 // Получение списка существующих пакетов в каталоге для генерации 498 // Необходимо для составления списка пакетов на удаление после генерации 499 func (a *ArGen) getExists() ([]string, error) { 500 existsFile := []string{} 501 502 // Проходим по всем каталогам результирующей директории 503 // По сути это пакеты, которые были сгенерированы в прошлый раз 504 for _, dstFile := range a.dstEntry { 505 if dstFile.Name() == ".argen" { 506 continue 507 } 508 509 if dstFile.Name() == "repository.go" { 510 continue 511 } 512 513 if !dstFile.Type().IsDir() { 514 return nil, fmt.Errorf("destination folder can contain only dirs. `%s` not a dir", dstFile.Name()) 515 } 516 517 dstRepoDir := filepath.Join(a.dst, dstFile.Name()) 518 519 existsFile = append(existsFile, dstRepoDir) 520 521 goFiles, err := os.ReadDir(dstRepoDir) 522 if err != nil { 523 return nil, fmt.Errorf("can'r read destination repository folder(%s): %s", dstFile.Name(), err) 524 } 525 526 for _, goFile := range goFiles { 527 if !strings.HasSuffix(dstFile.Name(), ".go") { 528 if goFile.Type().IsDir() || !goFile.Type().IsRegular() { 529 return nil, fmt.Errorf("destination repository folder can contain only go files. `%s` is not a go-file", goFile.Name()) 530 } 531 532 existsFile = append(existsFile, filepath.Join(dstRepoDir, goFile.Name())) 533 } 534 } 535 } 536 537 return existsFile, nil 538 } 539 540 func (a *ArGen) prepareMetaData() (generator.MetaData, error) { 541 metadata := generator.MetaData{ 542 AppInfo: a.appInfo.String(), 543 } 544 545 for _, cl := range a.packagesParsed { 546 for _, backend := range cl.Backends { 547 switch backend { 548 case "tarantool15": 549 fallthrough 550 case "octopus": 551 metadata.Namespaces = append(metadata.Namespaces, cl) 552 } 553 } 554 } 555 556 return metadata, nil 557 }