github.com/CiscoM31/godata@v1.0.10/compute_parser.go (about)

     1  package godata
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"regexp"
     8  	"strings"
     9  )
    10  
    11  // The $compute query option must have a value which is a comma separated list of <expression> as <dynamic property name>
    12  // See https://docs.oasis-open.org/odata/odata/v4.01/os/part2-url-conventions/odata-v4.01-os-part2-url-conventions.html#sec_SystemQueryOptioncompute
    13  const computeAsSeparator = " as "
    14  
    15  // Dynamic property names are restricted to case-insensitive a-z and the path separator /.
    16  var computeFieldRegex = regexp.MustCompile("^[a-zA-Z/]+$")
    17  
    18  type ComputeItem struct {
    19  	Tree  *ParseNode // The compute expression parsed as a tree.
    20  	Field string     // The name of the computed dynamic property.
    21  }
    22  
    23  // GlobalAllTokenParser is a Tokenizer which matches all tokens and ignores none. It differs from the
    24  // GlobalExpressionTokenizer which ignores whitespace tokens.
    25  var GlobalAllTokenParser *Tokenizer
    26  
    27  func init() {
    28  	t := NewExpressionParser().tokenizer
    29  	t.TokenMatchers = append(t.IgnoreMatchers, t.TokenMatchers...)
    30  	t.IgnoreMatchers = nil
    31  	GlobalAllTokenParser = t
    32  }
    33  
    34  func ParseComputeString(ctx context.Context, compute string) (*GoDataComputeQuery, error) {
    35  	items, err := SplitComputeItems(compute)
    36  	if err != nil {
    37  		return nil, err
    38  	}
    39  
    40  	result := make([]*ComputeItem, 0)
    41  	fields := map[string]struct{}{}
    42  
    43  	for _, v := range items {
    44  		v = strings.TrimSpace(v)
    45  		parts := strings.Split(v, computeAsSeparator)
    46  		if len(parts) != 2 {
    47  			return nil, &GoDataError{
    48  				ResponseCode: 400,
    49  				Message:      "Invalid $compute query option",
    50  			}
    51  		}
    52  		field := strings.TrimSpace(parts[1])
    53  		if !computeFieldRegex.MatchString(field) {
    54  			return nil, &GoDataError{
    55  				ResponseCode: 400,
    56  				Message:      "Invalid $compute query option",
    57  			}
    58  		}
    59  
    60  		if tree, err := GlobalExpressionParser.ParseExpressionString(ctx, parts[0]); err != nil {
    61  			switch e := err.(type) {
    62  			case *GoDataError:
    63  				return nil, &GoDataError{
    64  					ResponseCode: e.ResponseCode,
    65  					Message:      fmt.Sprintf("Invalid $compute query option, %s", e.Message),
    66  					Cause:        e,
    67  				}
    68  			default:
    69  				return nil, &GoDataError{
    70  					ResponseCode: 500,
    71  					Message:      "Invalid $compute query option",
    72  					Cause:        e,
    73  				}
    74  			}
    75  		} else {
    76  			if tree == nil {
    77  				return nil, &GoDataError{
    78  					ResponseCode: 500,
    79  					Message:      "Invalid $compute query option",
    80  				}
    81  			}
    82  
    83  			if _, ok := fields[field]; ok {
    84  				return nil, &GoDataError{
    85  					ResponseCode: 400,
    86  					Message:      "Invalid $compute query option",
    87  				}
    88  			}
    89  
    90  			fields[field] = struct{}{}
    91  
    92  			result = append(result, &ComputeItem{
    93  				Tree:  tree.Tree,
    94  				Field: field,
    95  			})
    96  		}
    97  	}
    98  
    99  	return &GoDataComputeQuery{result, compute}, nil
   100  }
   101  
   102  // SplitComputeItems splits the input string based on the comma delimiter. It does so with awareness as to
   103  // which commas delimit $compute items and which ones are an inline part of the item, such as a separator
   104  // for function arguments.
   105  //
   106  // For example the input "someFunc(one,two) as three, 1 add 2 as four" results in the
   107  // output ["someFunc(one,two) as three", "1 add 2 as four"]
   108  func SplitComputeItems(in string) ([]string, error) {
   109  
   110  	var ret []string
   111  
   112  	tokens, err := GlobalAllTokenParser.Tokenize(context.Background(), in)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  
   117  	item := strings.Builder{}
   118  	parenGauge := 0
   119  
   120  	for _, v := range tokens {
   121  		switch v.Type {
   122  		case ExpressionTokenOpenParen:
   123  			parenGauge++
   124  		case ExpressionTokenCloseParen:
   125  			if parenGauge == 0 {
   126  				return nil, errors.New("unmatched parentheses")
   127  			}
   128  			parenGauge--
   129  		case ExpressionTokenComma:
   130  			if parenGauge == 0 {
   131  				ret = append(ret, item.String())
   132  				item.Reset()
   133  				continue
   134  			}
   135  		}
   136  
   137  		item.WriteString(v.Value)
   138  	}
   139  
   140  	if parenGauge != 0 {
   141  		return nil, errors.New("unmatched parentheses")
   142  	}
   143  
   144  	if item.Len() > 0 {
   145  		ret = append(ret, item.String())
   146  	}
   147  
   148  	return ret, nil
   149  }