github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/project/expander.go (about)

     1  package project
     2  
     3  import (
     4  	"path/filepath"
     5  	"regexp"
     6  	"runtime"
     7  	"strings"
     8  
     9  	"github.com/ActiveState/cli/internal/constants"
    10  	"github.com/ActiveState/cli/internal/constraints"
    11  	"github.com/ActiveState/cli/internal/errs"
    12  	"github.com/ActiveState/cli/internal/language"
    13  	"github.com/ActiveState/cli/internal/locale"
    14  	"github.com/ActiveState/cli/internal/osutils"
    15  	"github.com/ActiveState/cli/internal/rxutils"
    16  	"github.com/ActiveState/cli/internal/scriptfile"
    17  	"github.com/ActiveState/cli/pkg/platform/authentication"
    18  	"github.com/ActiveState/cli/pkg/projectfile"
    19  )
    20  
    21  type Expansion struct {
    22  	Project      *Project
    23  	Script       *Script
    24  	BashifyPaths bool
    25  }
    26  
    27  func NewExpansion(p *Project) *Expansion {
    28  	return &Expansion{Project: p}
    29  }
    30  
    31  // ApplyWithMaxDepth limits the depth of an expansion to avoid infinite expansion of a value.
    32  func (ctx *Expansion) ApplyWithMaxDepth(s string, depth int) (string, error) {
    33  	if depth > constants.ExpanderMaxDepth {
    34  		return "", locale.NewExternalError("err_expand_recursion", "Infinite recursion trying to expand variable '{{.V0}}'", s)
    35  	}
    36  
    37  	regex := regexp.MustCompile(`\${?(\w+)\.?([\w-]+)?\.?([\w\.-]+)?(\(\))?}?`)
    38  	var err error
    39  	expanded := rxutils.ReplaceAllStringSubmatchFunc(regex, s, func(groups []string) string {
    40  		if err != nil {
    41  			return ""
    42  		}
    43  		var variable, category, name, meta string
    44  		var isFunction bool
    45  		variable = groups[0]
    46  
    47  		if len(groups) == 2 {
    48  			category = "toplevel"
    49  			name = groups[1]
    50  		}
    51  		if len(groups) > 2 {
    52  			category = groups[1]
    53  			name = groups[2]
    54  		}
    55  		if len(groups) > 3 {
    56  			meta = groups[3]
    57  		}
    58  		lastGroup := groups[len(groups)-1]
    59  		if strings.HasPrefix(lastGroup, "(") && strings.HasSuffix(lastGroup, ")") {
    60  			isFunction = true
    61  		}
    62  
    63  		var value string
    64  
    65  		if expanderFn, foundExpander := expanderRegistry[category]; foundExpander {
    66  			var err2 error
    67  			if value, err2 = expanderFn(variable, name, meta, isFunction, ctx); err2 != nil {
    68  				err = errs.Wrap(err2, "Could not expand %s.%s", category, name)
    69  				return ""
    70  			}
    71  		} else {
    72  			return variable // we don't control this variable, so leave it as is
    73  		}
    74  
    75  		if value != "" && value != variable {
    76  			value, err = ctx.ApplyWithMaxDepth(value, depth+1)
    77  		}
    78  		return value
    79  	})
    80  
    81  	return expanded, err
    82  }
    83  
    84  // ExpandFromProject searches for $category.name-style variables in the given
    85  // string and substitutes them with their contents, derived from the given
    86  // project, and subject to the given constraints (if any).
    87  func ExpandFromProject(s string, p *Project) (string, error) {
    88  	return NewExpansion(p).ApplyWithMaxDepth(s, 0)
    89  }
    90  
    91  // ExpandFromProjectBashifyPaths is like ExpandFromProject, but bashifies all instances of
    92  // $script.name.path().
    93  func ExpandFromProjectBashifyPaths(s string, p *Project) (string, error) {
    94  	expansion := &Expansion{Project: p, BashifyPaths: true}
    95  	return expansion.ApplyWithMaxDepth(s, 0)
    96  }
    97  
    98  func ExpandFromScript(s string, script *Script) (string, error) {
    99  	expansion := &Expansion{
   100  		Project:      script.project,
   101  		Script:       script,
   102  		BashifyPaths: runtime.GOOS == "windows" && (script.LanguageSafe()[0] == language.Bash || script.LanguageSafe()[0] == language.Sh),
   103  	}
   104  	return expansion.ApplyWithMaxDepth(s, 0)
   105  }
   106  
   107  // ExpanderFunc defines an Expander function which can expand the name for a category. An Expander expects the name
   108  // to be expanded along with the project-file definition. It will return the expanded value of the name
   109  // or a Failure if expansion was unsuccessful.
   110  type ExpanderFunc func(variable, name, meta string, isFunction bool, ctx *Expansion) (string, error)
   111  
   112  // EventExpander expands events defined in the project-file.
   113  func EventExpander(_ string, name string, meta string, isFunction bool, ctx *Expansion) (string, error) {
   114  	projectFile := ctx.Project.Source()
   115  	constrained, err := constraints.FilterUnconstrained(pConditional, projectFile.Events.AsConstrainedEntities())
   116  	if err != nil {
   117  		return "", err
   118  	}
   119  	for _, v := range constrained {
   120  		if v.ID() == name {
   121  			return projectfile.MakeEventsFromConstrainedEntities([]projectfile.ConstrainedEntity{v})[0].Value, nil
   122  		}
   123  	}
   124  	return "", nil
   125  }
   126  
   127  // ScriptExpander expands scripts defined in the project-file.
   128  func ScriptExpander(_ string, name string, meta string, isFunction bool, ctx *Expansion) (string, error) {
   129  	script := ctx.Project.ScriptByName(name)
   130  	if script == nil {
   131  		return "", nil
   132  	}
   133  
   134  	if !isFunction {
   135  		return script.Raw(), nil
   136  	}
   137  
   138  	if meta == "path" || meta == "path._posix" {
   139  		path, err := expandPath(name, script)
   140  		if err != nil {
   141  			return "", err
   142  		}
   143  
   144  		if ctx.BashifyPaths || meta == "path._posix" {
   145  			return osutils.BashifyPath(path)
   146  		}
   147  
   148  		return path, nil
   149  	}
   150  
   151  	return script.Raw(), nil
   152  }
   153  
   154  func expandPath(name string, script *Script) (string, error) {
   155  	if script.cachedFile() != "" {
   156  		return script.cachedFile(), nil
   157  	}
   158  
   159  	languages := script.LanguageSafe()
   160  	if len(languages) == 0 {
   161  		languages = DefaultScriptLanguage()
   162  	}
   163  
   164  	sf, err := scriptfile.NewEmpty(languages[0], name)
   165  	if err != nil {
   166  		return "", err
   167  	}
   168  	script.setCachedFile(sf.Filename())
   169  
   170  	v, err := script.Value()
   171  	if err != nil {
   172  		return "", err
   173  	}
   174  	err = sf.Write(v)
   175  	if err != nil {
   176  		return "", err
   177  	}
   178  
   179  	return sf.Filename(), nil
   180  }
   181  
   182  // userExpander
   183  func userExpander(auth *authentication.Auth, element string) string {
   184  	if element == "name" {
   185  		return auth.WhoAmI()
   186  	}
   187  	if element == "email" {
   188  		return auth.Email()
   189  	}
   190  	if element == "jwt" {
   191  		return auth.BearerToken()
   192  	}
   193  	return ""
   194  }
   195  
   196  // Mixin provides expansions that are not sourced from a project file
   197  type Mixin struct {
   198  	auth *authentication.Auth
   199  }
   200  
   201  // NewMixin creates a Mixin object providing extra expansions
   202  func NewMixin(auth *authentication.Auth) *Mixin {
   203  	return &Mixin{auth}
   204  }
   205  
   206  // Expander expands mixin variables
   207  func (m *Mixin) Expander(_ string, name string, meta string, _ bool, _ *Expansion) (string, error) {
   208  	if name == "user" {
   209  		return userExpander(m.auth, meta), nil
   210  	}
   211  	return "", nil
   212  }
   213  
   214  // ConstantExpander expands constants defined in the project-file.
   215  func ConstantExpander(_ string, name string, meta string, isFunction bool, ctx *Expansion) (string, error) {
   216  	projectFile := ctx.Project.Source()
   217  	constrained, err := constraints.FilterUnconstrained(pConditional, projectFile.Constants.AsConstrainedEntities())
   218  	if err != nil {
   219  		return "", err
   220  	}
   221  	for _, v := range constrained {
   222  		if v.ID() == name {
   223  			return projectfile.MakeConstantsFromConstrainedEntities([]projectfile.ConstrainedEntity{v})[0].Value, nil
   224  		}
   225  	}
   226  	return "", nil
   227  }
   228  
   229  // ProjectExpander expands constants defined in the project-file.
   230  func ProjectExpander(_ string, name string, _ string, isFunction bool, ctx *Expansion) (string, error) {
   231  	if !isFunction {
   232  		return "", nil
   233  	}
   234  
   235  	project := ctx.Project
   236  	switch name {
   237  	case "url":
   238  		return project.URL(), nil
   239  	case "commit":
   240  		commitID := project.LegacyCommitID() // Not using localcommit due to import cycle. See anti-pattern comment in localcommit pkg.
   241  		return commitID, nil
   242  	case "branch":
   243  		return project.BranchName(), nil
   244  	case "owner":
   245  		return project.Namespace().Owner, nil
   246  	case "name":
   247  		return project.Namespace().Project, nil
   248  	case "namespace":
   249  		return project.Namespace().String(), nil
   250  	case "path":
   251  		path := project.Source().Path()
   252  		if path == "" {
   253  			return path, nil
   254  		}
   255  		dir := filepath.Dir(path)
   256  		if ctx.BashifyPaths {
   257  			return osutils.BashifyPath(dir)
   258  		}
   259  		return dir, nil
   260  	}
   261  
   262  	return "", nil
   263  }
   264  
   265  func TopLevelExpander(variable string, name string, _ string, _ bool, ctx *Expansion) (string, error) {
   266  	projectFile := ctx.Project.Source()
   267  	switch name {
   268  	case "project":
   269  		return projectFile.Project, nil
   270  	case "lock":
   271  		return projectFile.Lock, nil
   272  	}
   273  	return variable, nil
   274  }