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  }