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

     1  /*
     2   * Copyright (c) 2023-present unTill Pro, Ltd.
     3   * @author Alisher Nurmanov
     4   */
     5  
     6  package main
     7  
     8  import (
     9  	"errors"
    10  	"fmt"
    11  	"os"
    12  	"path/filepath"
    13  	"strings"
    14  
    15  	"github.com/spf13/cobra"
    16  	"gopkg.in/yaml.v2"
    17  
    18  	"github.com/voedger/voedger/pkg/appdef"
    19  	"github.com/voedger/voedger/pkg/appdefcompat"
    20  	"github.com/voedger/voedger/pkg/compile"
    21  	"github.com/voedger/voedger/pkg/parser"
    22  	coreutils "github.com/voedger/voedger/pkg/utils"
    23  )
    24  
    25  func newCompatCmd(params *vpmParams) *cobra.Command {
    26  	cmd := &cobra.Command{
    27  		Use:   "compat baseline-folder",
    28  		Short: "check backward compatibility",
    29  		Args:  exactArgs(1),
    30  		RunE: func(cmd *cobra.Command, args []string) (err error) {
    31  			ignores, err := readIgnoreFile(params.IgnoreFile)
    32  			if err != nil {
    33  				return err
    34  			}
    35  			compileRes, err := compile.Compile(params.Dir)
    36  			if err != nil {
    37  				return err
    38  			}
    39  			return compat(compileRes, params, ignores)
    40  		},
    41  	}
    42  	cmd.Flags().StringVarP(&params.IgnoreFile, "ignore", "", "", "path to yaml file which contains list of errors to be ignored")
    43  	return cmd
    44  }
    45  
    46  // compat checks compatibility of schemas in dir versus baseline schemas in target dir
    47  func compat(compileRes *compile.Result, params *vpmParams, ignores [][]string) error {
    48  	baselineDir := params.TargetDir
    49  	var errs []error
    50  	baselineAppDef, err := appDefFromBaselineDir(baselineDir)
    51  	if err != nil {
    52  		errs = append(errs, coreutils.SplitErrors(err)...)
    53  	}
    54  
    55  	if baselineAppDef != nil && compileRes.AppDef != nil {
    56  		compatErrs := appdefcompat.CheckBackwardCompatibility(baselineAppDef, compileRes.AppDef)
    57  		compatErrs = appdefcompat.IgnoreCompatibilityErrors(compatErrs, ignores)
    58  		errObjs := make([]error, len(compatErrs.Errors))
    59  		for i, err := range compatErrs.Errors {
    60  			errObjs[i] = err
    61  		}
    62  		errs = append(errs, errObjs...)
    63  	}
    64  	return errors.Join(errs...)
    65  }
    66  
    67  // readIgnoreFile reads yaml file and returns list of errors to be ignored
    68  func readIgnoreFile(ignoreFilePath string) ([][]string, error) {
    69  	if ignoreFilePath != "" {
    70  		content, err := os.ReadFile(ignoreFilePath)
    71  		if err != nil {
    72  			return nil, err
    73  		}
    74  
    75  		var ignoreInfoObj ignoreInfo
    76  		if err := yaml.Unmarshal(content, &ignoreInfoObj); err != nil {
    77  			return nil, err
    78  		}
    79  		return splitIgnorePaths(ignoreInfoObj.Ignore), nil
    80  	}
    81  	return nil, nil
    82  }
    83  
    84  // appDefFromBaselineDir builds app def from baseline dir
    85  func appDefFromBaselineDir(baselineDir string) (appdef.IAppDef, error) {
    86  	var errs []error
    87  
    88  	pkgDirPath := filepath.Join(baselineDir, pkgDirName)
    89  	pkgDirPathExists, err := coreutils.Exists(pkgDirPath)
    90  	if err != nil {
    91  		// notest
    92  		return nil, err
    93  	}
    94  	if !pkgDirPathExists {
    95  		return nil, fmt.Errorf("baseline directory does not contain %s subdirectory", pkgDirName)
    96  	}
    97  	baselineJsonFilePath := filepath.Join(baselineDir, baselineInfoFileName)
    98  	baselineJsonFilePathExists, err := coreutils.Exists(baselineJsonFilePath)
    99  	if err != nil {
   100  		// notest
   101  		return nil, err
   102  	}
   103  	if !baselineJsonFilePathExists {
   104  		return nil, fmt.Errorf("baseline directory does not contain %s file", baselineInfoFileName)
   105  	}
   106  
   107  	// gather schema files from baseline dir
   108  	var schemaFiles []string
   109  	if err := filepath.Walk(pkgDirPath, func(path string, info os.FileInfo, err error) error {
   110  		if err != nil {
   111  			return err
   112  		}
   113  		if !info.IsDir() && filepath.Ext(path) == parser.VSqlExt {
   114  			schemaFiles = append(schemaFiles, path)
   115  		}
   116  		return nil
   117  	}); err != nil {
   118  		errs = append(errs, err)
   119  	}
   120  
   121  	// form package files structure
   122  	pkgFiles := make(packageFiles)
   123  	for _, schemaFile := range schemaFiles {
   124  		dir := filepath.Dir(schemaFile)
   125  		qpn, err := filepath.Rel(pkgDirPath, dir)
   126  		if err != nil {
   127  			return nil, err
   128  		}
   129  		qpn = strings.ReplaceAll(qpn, "\\", "/")
   130  		pkgFiles[qpn] = append(pkgFiles[qpn], schemaFile)
   131  	}
   132  
   133  	// build package ASTs from schema files
   134  	packageASTs := make([]*parser.PackageSchemaAST, 0)
   135  	for qpn, files := range pkgFiles {
   136  		// build file ASTs
   137  		var fileASTs []*parser.FileSchemaAST
   138  		for _, file := range files {
   139  			content, err := os.ReadFile(file)
   140  			if err != nil {
   141  				errs = append(errs, err)
   142  			}
   143  			fileName := filepath.Base(file)
   144  
   145  			fileAST, err := parser.ParseFile(fileName, string(content))
   146  			if err != nil {
   147  				errs = append(errs, err)
   148  			}
   149  			fileASTs = append(fileASTs, fileAST)
   150  		}
   151  
   152  		// build package AST
   153  		packageAST, err := parser.BuildPackageSchema(qpn, fileASTs)
   154  		if err != nil {
   155  			errs = append(errs, err)
   156  		}
   157  		// add package AST to list
   158  		packageASTs = append(packageASTs, packageAST)
   159  	}
   160  
   161  	// build app AST
   162  	appAST, err := parser.BuildAppSchema(packageASTs)
   163  	if err != nil {
   164  		errs = append(errs, err)
   165  	}
   166  	// build app def from app AST
   167  	if appAST != nil {
   168  		builder := appdef.New()
   169  		if err := parser.BuildAppDefs(appAST, builder); err != nil {
   170  			errs = append(errs, err)
   171  		}
   172  		appDef, err := builder.Build()
   173  		if err != nil {
   174  			errs = append(errs, err)
   175  		}
   176  		return appDef, errors.Join(errs...)
   177  	}
   178  	return nil, errors.Join(errs...)
   179  }
   180  
   181  // splitIgnorePaths splits list of ignore paths into list of path parts
   182  func splitIgnorePaths(ignores []string) (res [][]string) {
   183  	res = make([][]string, len(ignores))
   184  	for i, ignore := range ignores {
   185  		res[i] = strings.Split(ignore, "/")
   186  	}
   187  	return
   188  }