pkg/internal/expression/interpolation.go (108 lines of code) (raw):
package expression
import (
"fmt"
"regexp"
"google.golang.org/protobuf/types/known/structpb"
"gitlab.com/gitlab-org/step-runner/pkg/context"
)
const InterpolateOpen = "${{"
const InterpolateClose = "}}"
var interpolateRegex = regexp.MustCompile(regexp.QuoteMeta(InterpolateOpen) + "|" + regexp.QuoteMeta(InterpolateClose))
func interpolateString(obj *InterpolationContext, value string) (*context.Value, error) {
output := []*structpb.Value{}
depth := 0
prev_idx := 0
open_idx := 0
sensitive := false
sensitiveReasons := make([]string, 0)
for _, loc := range interpolateRegex.FindAllStringIndex(value, -1) {
if depth == 0 && prev_idx != loc[0] {
// add prefix to output
output = append(output, structpb.NewStringValue(value[prev_idx:loc[0]]))
}
prev_idx = loc[1]
switch value[loc[0]:loc[1]] {
case InterpolateOpen:
depth += 1
if depth == 1 {
open_idx = loc[1]
}
case InterpolateClose:
depth -= 1
insideString := value[open_idx:loc[0]]
if depth < 0 {
return nil, fmt.Errorf("The %q has extra '}}'", insideString)
} else if depth > 0 {
break
}
insideValue, err := Evaluate(obj, insideString)
if err != nil {
return nil, err
}
if insideValue.Sensitive {
sensitive = true
sensitiveReasons = append(sensitiveReasons, insideValue.SensitiveReason)
}
output = append(output, insideValue.Value)
default:
}
}
if depth > 0 {
return nil, fmt.Errorf("The %q is not closed: ${{ ... }}", value[open_idx:])
}
// add suffix to output
if prev_idx != len(value) {
output = append(output, structpb.NewStringValue(value[prev_idx:]))
}
// retain type if this is single item, otherwise convert to string
if len(output) == 1 {
return context.NewValue(output[0], sensitive, sensitiveReasons...), nil
}
// concat all items
res := ""
for _, o := range output {
str, err := ValueToString(o)
if err != nil {
return nil, err
}
res += str
}
return context.NewStringValue(res, sensitive, sensitiveReasons...), nil
}
func expandStruct(obj *InterpolationContext, value *structpb.Struct) (*context.Value, error) {
res := &structpb.Struct{Fields: make(map[string]*structpb.Value, len(value.Fields))}
for fieldKey, fieldValue := range value.Fields {
fieldNewValue, err := Expand(obj, fieldValue)
if err != nil {
return nil, err
}
res.Fields[fieldKey] = fieldNewValue.Value
}
return context.NewNonSensitiveValue(structpb.NewStructValue(res)), nil
}
func expandList(obj *InterpolationContext, value *structpb.ListValue) (*context.Value, error) {
res := &structpb.ListValue{Values: make([]*structpb.Value, len(value.Values))}
for listIndex, listValue := range value.Values {
listNewValue, err := Expand(obj, listValue)
if err != nil {
return nil, err
}
res.Values[listIndex] = listNewValue.Value
}
return context.NewNonSensitiveValue(structpb.NewListValue(res)), nil
}
// The Expand rewrites struct/list/string mutating data structure
func Expand(obj *InterpolationContext, value *structpb.Value) (*context.Value, error) {
switch value.Kind.(type) {
case *structpb.Value_StringValue:
return interpolateString(obj, value.GetStringValue())
case *structpb.Value_StructValue:
return expandStruct(obj, value.GetStructValue())
case *structpb.Value_ListValue:
return expandList(obj, value.GetListValue())
default:
return context.NewNonSensitiveValue(value), nil
}
}
// The ExpandString rewrites string and returns string
func ExpandString(obj *InterpolationContext, value string) (string, error) {
res, err := interpolateString(obj, value)
if err != nil {
return "", err
}
return ValueToString(res.Value)
}