github.com/kubeshop/testkube@v1.17.23/pkg/tcl/expressionstcl/parse.go (about)

     1  // Copyright 2024 Testkube.
     2  //
     3  // Licensed as a Testkube Pro file under the Testkube Community
     4  // License (the "License"); you may not use this file except in compliance with
     5  // the License. You may obtain a copy of the License at
     6  //
     7  //     https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
     8  
     9  package expressionstcl
    10  
    11  import (
    12  	"errors"
    13  	"fmt"
    14  	math2 "math"
    15  	"regexp"
    16  	"strings"
    17  )
    18  
    19  func parseNextExpression(t []token, priority int) (e Expression, i int, err error) {
    20  	e, i, err = getNextSegment(t)
    21  	if err != nil {
    22  		return
    23  	}
    24  
    25  	for {
    26  		// End of the expression
    27  		if len(t) == i {
    28  			return e, i, nil
    29  		}
    30  
    31  		switch t[i].Type {
    32  		case tokenTypeTernary:
    33  			if priority >= 0 {
    34  				return e, i, nil
    35  			}
    36  			i += 1
    37  			te, ti, terr := parseNextExpression(t[i:], 0)
    38  			i += ti
    39  			if terr != nil {
    40  				return nil, i, terr
    41  			}
    42  			if len(t) == i {
    43  				return nil, i, fmt.Errorf("premature end of expression: expected ternary separator")
    44  			}
    45  			if t[i].Type != tokenTypeTernarySeparator {
    46  				return nil, i, fmt.Errorf("expression syntax error: expected ternary separator: found %v", t[i])
    47  			}
    48  			i++
    49  			fe, fi, ferr := parseNextExpression(t[i:], 0)
    50  			i += fi
    51  			if ferr != nil {
    52  				return nil, i, ferr
    53  			}
    54  			e = newConditional(e, te, fe)
    55  		case tokenTypeMath:
    56  			op := operator(t[i].Value.(string))
    57  			nextPriority := getOperatorPriority(op)
    58  			if priority >= nextPriority {
    59  				return e, i, nil
    60  			}
    61  			i += 1
    62  			ne, ni, nerr := parseNextExpression(t[i:], nextPriority)
    63  			i += ni
    64  			if nerr != nil {
    65  				return nil, i, nerr
    66  			}
    67  			e = newMath(op, e, ne)
    68  		case tokenTypePropertyAccessor:
    69  			e = newPropertyAccessor(e, t[i].Value.(string))
    70  			i += 1
    71  		default:
    72  			return e, i, err
    73  		}
    74  	}
    75  }
    76  
    77  func getNextSegment(t []token) (e Expression, i int, err error) {
    78  	if len(t) == 0 {
    79  		return nil, 0, errors.New("premature end of expression")
    80  	}
    81  
    82  	// Parentheses - (a(b) + c)
    83  	if t[0].Type == tokenTypeOpen {
    84  		e, i, err = parseNextExpression(t[1:], -1)
    85  		i++
    86  		if err != nil {
    87  			return nil, i, err
    88  		}
    89  		if len(t) <= i || t[i].Type != tokenTypeClose {
    90  			return nil, i, fmt.Errorf("syntax error: expected parentheses close")
    91  		}
    92  		return e, i + 1, err
    93  	}
    94  
    95  	// Static value - "abc", 444, {"a": 10}, true, [45, 3]
    96  	if t[0].Type == tokenTypeJson {
    97  		return NewValue(t[0].Value), 1, nil
    98  	}
    99  
   100  	// Negation - !expr
   101  	if t[0].Type == tokenTypeNot {
   102  		e, i, err = parseNextExpression(t[1:], math2.MaxInt)
   103  		if err != nil {
   104  			return nil, 0, err
   105  		}
   106  		return newNegative(e), i + 1, nil
   107  	}
   108  
   109  	// Negative numbers - -5
   110  	if t[0].Type == tokenTypeMath && operator(t[0].Value.(string)) == operatorSubtract {
   111  		e, i, err = parseNextExpression(t[1:], -1)
   112  		if err != nil {
   113  			return nil, 0, err
   114  		}
   115  		return newMath(operatorSubtract, NewValue(0), e), i + 1, nil
   116  	}
   117  
   118  	// Call - abc(a, b, c)
   119  	if t[0].Type == tokenTypeAccessor && len(t) > 1 && t[1].Type == tokenTypeOpen {
   120  		args := make([]callArgument, 0)
   121  		index := 2
   122  		for {
   123  			// Ensure there is another token (for call close or next argument)
   124  			if len(t) <= index {
   125  				return nil, 2, errors.New("premature end of expression: missing call close")
   126  			}
   127  
   128  			// Close the call
   129  			if t[index].Type == tokenTypeClose {
   130  				break
   131  			}
   132  
   133  			// Ensure comma between arguments
   134  			if len(args) != 0 {
   135  				if t[index].Type != tokenTypeComma {
   136  					return nil, 2, errors.New("expression syntax error: expected comma or call close")
   137  				}
   138  				index++
   139  			}
   140  			next, l, err := parseNextExpression(t[index:], -1)
   141  			index += l
   142  			if err != nil {
   143  				return nil, index, err
   144  			}
   145  			if len(t) > index && t[index].Type == tokenTypeSpread {
   146  				args = append(args, callArgument{expr: next, spread: true})
   147  				index++
   148  			} else {
   149  				args = append(args, callArgument{expr: next})
   150  			}
   151  		}
   152  		return newCall(t[0].Value.(string), args), index + 1, nil
   153  	}
   154  
   155  	// Accessor - abc
   156  	if t[0].Type == tokenTypeAccessor {
   157  		return newAccessor(t[0].Value.(string)), 1, nil
   158  	}
   159  
   160  	return nil, 0, fmt.Errorf("unexpected token in expression: %v", t)
   161  }
   162  
   163  func parse(t []token) (e Expression, err error) {
   164  	if len(t) == 0 {
   165  		return None, nil
   166  	}
   167  	e, l, err := parseNextExpression(t, -1)
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  	if l < len(t) {
   172  		return nil, fmt.Errorf("unexpected token after end of expression: %v", t[l])
   173  	}
   174  	return e, nil
   175  }
   176  
   177  func Compile(exp string) (Expression, error) {
   178  	t, _, e := tokenize(exp, 0)
   179  	if e != nil {
   180  		return nil, fmt.Errorf("tokenizer error: %v", e)
   181  	}
   182  	v, e := parse(t)
   183  	if e != nil {
   184  		return nil, fmt.Errorf("parser error: %v", e)
   185  	}
   186  	return v.Resolve()
   187  }
   188  
   189  func MustCompile(exp string) Expression {
   190  	v, err := Compile(exp)
   191  	if err != nil {
   192  		panic(err)
   193  	}
   194  	return v
   195  }
   196  
   197  var endExprRe = regexp.MustCompile(`^\s*}}`)
   198  
   199  func CompileTemplate(tpl string) (Expression, error) {
   200  	var e Expression
   201  
   202  	offset := 0
   203  	for index := strings.Index(tpl[offset:], "{{"); index != -1; index = strings.Index(tpl[offset:], "{{") {
   204  		if index != 0 {
   205  			e = newMath(operatorAdd, e, NewStringValue(tpl[offset:offset+index]))
   206  		}
   207  		offset += index + 2
   208  		tokens, i, err := tokenize(tpl, offset)
   209  		offset = i
   210  		if err == nil {
   211  			return nil, errors.New("template error: expression not closed")
   212  		}
   213  		if !endExprRe.MatchString(tpl[offset:]) || !strings.Contains(err.Error(), "unknown character") {
   214  			return nil, fmt.Errorf("tokenizer error: %v", err)
   215  		}
   216  		offset += len(endExprRe.FindString(tpl[offset:]))
   217  		if len(tokens) == 0 {
   218  			continue
   219  		}
   220  		v, err := parse(tokens)
   221  		if err != nil {
   222  			return nil, fmt.Errorf("parser error: %v", e)
   223  		}
   224  		v, err = v.Resolve()
   225  		if err != nil {
   226  			return nil, fmt.Errorf("expression error: %v", e)
   227  		}
   228  		e = newMath(operatorAdd, e, CastToString(v))
   229  	}
   230  	if offset < len(tpl) {
   231  		e = newMath(operatorAdd, e, NewStringValue(tpl[offset:]))
   232  	}
   233  	if e == nil {
   234  		return NewStringValue(""), nil
   235  	}
   236  	return e.Resolve()
   237  }
   238  
   239  func MustCompileTemplate(tpl string) Expression {
   240  	v, err := CompileTemplate(tpl)
   241  	if err != nil {
   242  		panic(err)
   243  	}
   244  	return v
   245  }
   246  
   247  func CompileAndResolve(exp string, m ...Machine) (Expression, error) {
   248  	e, err := Compile(exp)
   249  	if err != nil {
   250  		return e, err
   251  	}
   252  	return e.Resolve(m...)
   253  }
   254  
   255  func CompileAndResolveTemplate(tpl string, m ...Machine) (Expression, error) {
   256  	e, err := CompileTemplate(tpl)
   257  	if err != nil {
   258  		return e, err
   259  	}
   260  	return e.Resolve(m...)
   261  }
   262  
   263  func IsTemplateStringWithoutExpressions(tpl string) bool {
   264  	return !strings.Contains(tpl, "{{")
   265  }