github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/steampipeconfig/parse/query_invocation.go (about)

     1  package parse
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/hashicorp/hcl/v2"
     8  	"github.com/hashicorp/hcl/v2/hclsyntax"
     9  	"github.com/turbot/pipe-fittings/hclhelpers"
    10  	"github.com/turbot/steampipe-plugin-sdk/v5/plugin"
    11  	"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
    12  )
    13  
    14  // ParseQueryInvocation parses a query invocation and extracts the args (if any)
    15  // supported formats are:
    16  //
    17  // 1) positional args
    18  // query.my_prepared_statement('val1','val1')
    19  //
    20  // 2) named args
    21  // query.my_prepared_statement(my_arg1 => 'test', my_arg2 => 'test2')
    22  func ParseQueryInvocation(arg string) (string, *modconfig.QueryArgs, error) {
    23  	// TODO strip non printing chars
    24  	args := &modconfig.QueryArgs{}
    25  
    26  	arg = strings.TrimSpace(arg)
    27  	query := arg
    28  	var err error
    29  	openBracketIdx := strings.Index(arg, "(")
    30  	closeBracketIdx := strings.LastIndex(arg, ")")
    31  	if openBracketIdx != -1 && closeBracketIdx == len(arg)-1 {
    32  		argsString := arg[openBracketIdx+1 : len(arg)-1]
    33  		args, err = parseArgs(argsString)
    34  		query = strings.TrimSpace(arg[:openBracketIdx])
    35  	}
    36  	return query, args, err
    37  }
    38  
    39  // parse the actual args string, i.e. the contents of the bracket
    40  // supported formats are:
    41  //
    42  // 1) positional args
    43  // 'val1','val1'
    44  //
    45  // 2) named args
    46  // my_arg1 => 'val1', my_arg2 => 'val2'
    47  func parseArgs(argsString string) (*modconfig.QueryArgs, error) {
    48  	res := modconfig.NewQueryArgs()
    49  	if len(argsString) == 0 {
    50  		return res, nil
    51  	}
    52  
    53  	// split on comma to get each arg string (taking quotes and brackets into account)
    54  	splitArgs, err := splitArgString(argsString)
    55  	if err != nil {
    56  		// return empty result, even if we have an error
    57  		return res, err
    58  	}
    59  
    60  	// first check for named args
    61  	argMap, err := parseNamedArgs(splitArgs)
    62  	if err != nil {
    63  		return res, err
    64  	}
    65  	if err := res.SetArgMap(argMap); err != nil {
    66  		return res, err
    67  	}
    68  
    69  	if res.Empty() {
    70  		// no named args - fall back on positional
    71  		argList, err := parsePositionalArgs(splitArgs)
    72  		if err != nil {
    73  			return res, err
    74  		}
    75  		if err := res.SetArgList(argList); err != nil {
    76  			return res, err
    77  		}
    78  	}
    79  	// return empty result, even if we have an error
    80  	return res, err
    81  }
    82  
    83  func splitArgString(argsString string) ([]string, error) {
    84  	var argsList []string
    85  	openElements := map[string]int{
    86  		"quote":  0,
    87  		"curly":  0,
    88  		"square": 0,
    89  	}
    90  	var currentWord string
    91  	for _, c := range argsString {
    92  		// should we split - are we in a block
    93  		if c == ',' &&
    94  			openElements["quote"] == 0 && openElements["curly"] == 0 && openElements["square"] == 0 {
    95  			if len(currentWord) > 0 {
    96  				argsList = append(argsList, currentWord)
    97  				currentWord = ""
    98  			}
    99  		} else {
   100  			currentWord = currentWord + string(c)
   101  		}
   102  
   103  		// handle brackets and quotes
   104  		switch c {
   105  		case '{':
   106  			if openElements["quote"] == 0 {
   107  				openElements["curly"]++
   108  			}
   109  		case '}':
   110  			if openElements["quote"] == 0 {
   111  				openElements["curly"]--
   112  				if openElements["curly"] < 0 {
   113  					return nil, fmt.Errorf("bad arg syntax")
   114  				}
   115  			}
   116  		case '[':
   117  			if openElements["quote"] == 0 {
   118  				openElements["square"]++
   119  			}
   120  		case ']':
   121  			if openElements["quote"] == 0 {
   122  				openElements["square"]--
   123  				if openElements["square"] < 0 {
   124  					return nil, fmt.Errorf("bad arg syntax")
   125  				}
   126  			}
   127  		case '"':
   128  			if openElements["quote"] == 0 {
   129  				openElements["quote"] = 1
   130  			} else {
   131  				openElements["quote"] = 0
   132  			}
   133  		}
   134  	}
   135  	if len(currentWord) > 0 {
   136  		argsList = append(argsList, currentWord)
   137  	}
   138  	return argsList, nil
   139  }
   140  
   141  func parseArg(v string) (any, error) {
   142  	b, diags := hclsyntax.ParseExpression([]byte(v), "", hcl.Pos{})
   143  	if diags.HasErrors() {
   144  		return "", plugin.DiagsToError("bad arg syntax", diags)
   145  	}
   146  	val, diags := b.Value(nil)
   147  	if diags.HasErrors() {
   148  		return "", plugin.DiagsToError("bad arg syntax", diags)
   149  	}
   150  	return hclhelpers.CtyToGo(val)
   151  }
   152  
   153  func parseNamedArgs(argsList []string) (map[string]any, error) {
   154  	var res = make(map[string]any)
   155  	for _, p := range argsList {
   156  		argTuple := strings.Split(strings.TrimSpace(p), "=>")
   157  		if len(argTuple) != 2 {
   158  			// not all args have valid syntax - give up
   159  			return nil, nil
   160  		}
   161  		k := strings.TrimSpace(argTuple[0])
   162  		val, err := parseArg(argTuple[1])
   163  		if err != nil {
   164  			return nil, err
   165  		}
   166  		res[k] = val
   167  	}
   168  	return res, nil
   169  }
   170  
   171  func parsePositionalArgs(argsList []string) ([]any, error) {
   172  	// convert to pointer array
   173  	res := make([]any, len(argsList))
   174  	// just treat args as positional args
   175  	// strip spaces
   176  	for i, v := range argsList {
   177  		valStr, err := parseArg(v)
   178  		if err != nil {
   179  			return nil, err
   180  		}
   181  		res[i] = valStr
   182  	}
   183  
   184  	return res, nil
   185  }