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 }