github.com/CiscoM31/godata@v1.0.10/expand_parser.go (about) 1 package godata 2 3 import ( 4 "context" 5 "fmt" 6 "strconv" 7 ) 8 9 type ExpandTokenType int 10 11 func (e ExpandTokenType) Value() int { 12 return (int)(e) 13 } 14 15 const ( 16 ExpandTokenOpenParen ExpandTokenType = iota 17 ExpandTokenCloseParen 18 ExpandTokenNav 19 ExpandTokenComma 20 ExpandTokenSemicolon 21 ExpandTokenEquals 22 ExpandTokenLiteral 23 ) 24 25 var GlobalExpandTokenizer = ExpandTokenizer() 26 27 // Represents an item to expand in an OData query. Tracks the path of the entity 28 // to expand and also the filter, levels, and reference options, etc. 29 type ExpandItem struct { 30 Path []*Token 31 Filter *GoDataFilterQuery 32 At *GoDataFilterQuery 33 Search *GoDataSearchQuery 34 OrderBy *GoDataOrderByQuery 35 Skip *GoDataSkipQuery 36 Top *GoDataTopQuery 37 Select *GoDataSelectQuery 38 Compute *GoDataComputeQuery 39 Expand *GoDataExpandQuery 40 Levels int 41 } 42 43 func ExpandTokenizer() *Tokenizer { 44 t := Tokenizer{} 45 t.Add("^\\(", ExpandTokenOpenParen) 46 t.Add("^\\)", ExpandTokenCloseParen) 47 t.Add("^/", ExpandTokenNav) 48 t.Add("^,", ExpandTokenComma) 49 t.Add("^;", ExpandTokenSemicolon) 50 t.Add("^=", ExpandTokenEquals) 51 t.Add("^[a-zA-Z0-9_\\'\\.:\\$ \\*]+", ExpandTokenLiteral) 52 53 return &t 54 } 55 56 func ParseExpandString(ctx context.Context, expand string) (*GoDataExpandQuery, error) { 57 tokens, err := GlobalExpandTokenizer.Tokenize(ctx, expand) 58 59 if err != nil { 60 return nil, err 61 } 62 63 stack := tokenStack{} 64 queue := tokenQueue{} 65 items := make([]*ExpandItem, 0) 66 67 for len(tokens) > 0 { 68 token := tokens[0] 69 tokens = tokens[1:] 70 71 if token.Value == "(" { 72 queue.Enqueue(token) 73 stack.Push(token) 74 } else if token.Value == ")" { 75 queue.Enqueue(token) 76 stack.Pop() 77 } else if token.Value == "," { 78 if stack.Empty() { 79 // no paren on the stack, parse this item and start a new queue 80 item, err := ParseExpandItem(ctx, queue) 81 if err != nil { 82 return nil, err 83 } 84 items = append(items, item) 85 queue = tokenQueue{} 86 } else { 87 // this comma is inside a nested expression, keep it in the queue 88 queue.Enqueue(token) 89 } 90 } else { 91 queue.Enqueue(token) 92 } 93 } 94 95 if !stack.Empty() { 96 return nil, BadRequestError("Mismatched parentheses in expand clause.") 97 } 98 99 item, err := ParseExpandItem(ctx, queue) 100 if err != nil { 101 return nil, err 102 } 103 items = append(items, item) 104 105 return &GoDataExpandQuery{ExpandItems: items}, nil 106 } 107 108 func ParseExpandItem(ctx context.Context, input tokenQueue) (*ExpandItem, error) { 109 110 item := &ExpandItem{} 111 item.Path = []*Token{} 112 113 stack := &tokenStack{} 114 queue := &tokenQueue{} 115 116 for !input.Empty() { 117 token := input.Dequeue() 118 if token.Value == "(" { 119 if !stack.Empty() { 120 // this is a nested slash, it belongs on the queue 121 queue.Enqueue(token) 122 } else { 123 // top level slash means we're done parsing the path 124 item.Path = append(item.Path, queue.Dequeue()) 125 } 126 stack.Push(token) 127 } else if token.Value == ")" { 128 stack.Pop() 129 if !stack.Empty() { 130 // this is a nested slash, it belongs on the queue 131 queue.Enqueue(token) 132 } else { 133 // top level slash means we're done parsing the options 134 err := ParseExpandOption(ctx, queue, item) 135 if err != nil { 136 return nil, err 137 } 138 // reset the queue 139 queue = &tokenQueue{} 140 } 141 } else if token.Value == "/" && stack.Empty() { 142 if queue.Empty() { 143 // Disallow extra leading and intermediate slash, like /Product and Product//Info 144 return nil, BadRequestError("Empty path segment in expand clause.") 145 } 146 if input.Empty() { 147 // Disallow extra trailing slash, like Product/ 148 return nil, BadRequestError("Empty path segment in expand clause.") 149 } 150 // at root level, slashes separate path segments 151 item.Path = append(item.Path, queue.Dequeue()) 152 } else if token.Value == ";" && stack.Size == 1 { 153 // semicolons only split expand options at the first level 154 err := ParseExpandOption(ctx, queue, item) 155 if err != nil { 156 return nil, err 157 } 158 // reset the queue 159 queue = &tokenQueue{} 160 } else { 161 queue.Enqueue(token) 162 } 163 } 164 165 if !stack.Empty() { 166 return nil, BadRequestError("Mismatched parentheses in expand clause.") 167 } 168 169 if !queue.Empty() { 170 item.Path = append(item.Path, queue.Dequeue()) 171 } 172 173 cfg, hasComplianceConfig := ctx.Value(odataCompliance).(OdataComplianceConfig) 174 if !hasComplianceConfig { 175 // Strict ODATA compliance by default. 176 cfg = ComplianceStrict 177 } 178 179 if len(item.Path) == 0 && cfg&ComplianceIgnoreInvalidComma == 0 { 180 return nil, BadRequestError("Extra comma in $expand.") 181 } 182 183 return item, nil 184 } 185 186 func ParseExpandOption(ctx context.Context, queue *tokenQueue, item *ExpandItem) error { 187 head := queue.Dequeue().Value 188 if queue.Head == nil { 189 return BadRequestError("Invalid expand clause.") 190 } 191 queue.Dequeue() // drop the '=' from the front of the queue 192 body := queue.GetValue() 193 194 cfg, hasComplianceConfig := ctx.Value(odataCompliance).(OdataComplianceConfig) 195 if !hasComplianceConfig { 196 // Strict ODATA compliance by default. 197 cfg = ComplianceStrict 198 } 199 200 if cfg == ComplianceStrict { 201 // Enforce that only supported keywords are specified in expand. 202 // The $levels keyword supported within expand is checked explicitly in addition to 203 // keywords listed in supportedOdataKeywords[] which are permitted within expand and 204 // at the top level of the odata query. 205 if _, ok := supportedOdataKeywords[head]; !ok && head != "$levels" { 206 return BadRequestError(fmt.Sprintf("Unsupported item '%s' in expand clause.", head)) 207 } 208 } 209 210 if head == "$filter" { 211 filter, err := ParseFilterString(ctx, body) 212 if err == nil { 213 item.Filter = filter 214 } else { 215 return err 216 } 217 } 218 219 if head == "at" { 220 at, err := ParseFilterString(ctx, body) 221 if err == nil { 222 item.At = at 223 } else { 224 return err 225 } 226 } 227 228 if head == "$search" { 229 search, err := ParseSearchString(ctx, body) 230 if err == nil { 231 item.Search = search 232 } else { 233 return err 234 } 235 } 236 237 if head == "$orderby" { 238 orderby, err := ParseOrderByString(ctx, body) 239 if err == nil { 240 item.OrderBy = orderby 241 } else { 242 return err 243 } 244 } 245 246 if head == "$skip" { 247 skip, err := ParseSkipString(ctx, body) 248 if err == nil { 249 item.Skip = skip 250 } else { 251 return err 252 } 253 } 254 255 if head == "$top" { 256 top, err := ParseTopString(ctx, body) 257 if err == nil { 258 item.Top = top 259 } else { 260 return err 261 } 262 } 263 264 if head == "$select" { 265 sel, err := ParseSelectString(ctx, body) 266 if err == nil { 267 item.Select = sel 268 } else { 269 return err 270 } 271 } 272 273 if head == "$compute" { 274 comp, err := ParseComputeString(ctx, body) 275 if err == nil { 276 item.Compute = comp 277 } else { 278 return err 279 } 280 } 281 282 if head == "$expand" { 283 expand, err := ParseExpandString(ctx, body) 284 if err == nil { 285 item.Expand = expand 286 } else { 287 return err 288 } 289 } 290 291 if head == "$levels" { 292 i, err := strconv.Atoi(body) 293 if err != nil { 294 return err 295 } 296 item.Levels = i 297 } 298 299 return nil 300 } 301 302 func SemanticizeExpandQuery( 303 expand *GoDataExpandQuery, 304 service *GoDataService, 305 entity *GoDataEntityType, 306 ) error { 307 308 if expand == nil { 309 return nil 310 } 311 312 // Replace $levels with a nested expand clause 313 for _, item := range expand.ExpandItems { 314 if item.Levels > 0 { 315 if item.Expand == nil { 316 item.Expand = &GoDataExpandQuery{[]*ExpandItem{}} 317 } 318 // Future recursive calls to SemanticizeExpandQuery() will build out 319 // this expand tree completely 320 item.Expand.ExpandItems = append( 321 item.Expand.ExpandItems, 322 &ExpandItem{ 323 Path: item.Path, 324 Levels: item.Levels - 1, 325 }, 326 ) 327 item.Levels = 0 328 } 329 } 330 331 // we're gonna rebuild the items list, replacing wildcards where possible 332 // TODO: can we save the garbage collector some heartache? 333 newItems := []*ExpandItem{} 334 335 for _, item := range expand.ExpandItems { 336 if item.Path[0].Value == "*" { 337 // replace wildcard with a copy of every navigation property 338 for _, navProp := range service.NavigationPropertyLookup[entity] { 339 path := []*Token{{Value: navProp.Name, Type: ExpandTokenLiteral}} 340 newItem := &ExpandItem{ 341 Path: append(path, item.Path[1:]...), 342 Levels: item.Levels, 343 Expand: item.Expand, 344 } 345 newItems = append(newItems, newItem) 346 } 347 // TODO: check for duplicates? 348 } else { 349 newItems = append(newItems, item) 350 } 351 } 352 353 expand.ExpandItems = newItems 354 355 for _, item := range expand.ExpandItems { 356 err := semanticizeExpandItem(item, service, entity) 357 if err != nil { 358 return err 359 } 360 } 361 362 return nil 363 } 364 365 func semanticizeExpandItem( 366 item *ExpandItem, 367 service *GoDataService, 368 entity *GoDataEntityType, 369 ) error { 370 371 // TODO: allow multiple path segments in expand clause 372 // TODO: handle $ref 373 if len(item.Path) > 1 { 374 return NotImplementedError("Multiple path segments not currently supported in expand clauses.") 375 } 376 377 navProps := service.NavigationPropertyLookup[entity] 378 target := item.Path[len(item.Path)-1] 379 if prop, ok := navProps[target.Value]; ok { 380 target.SemanticType = SemanticTypeEntity 381 entityType, err := service.LookupEntityType(prop.Type) 382 if err != nil { 383 return err 384 } 385 target.SemanticReference = entityType 386 387 err = SemanticizeFilterQuery(item.Filter, service, entityType) 388 if err != nil { 389 return err 390 } 391 err = SemanticizeExpandQuery(item.Expand, service, entityType) 392 if err != nil { 393 return err 394 } 395 err = SemanticizeSelectQuery(item.Select, service, entityType) 396 if err != nil { 397 return err 398 } 399 err = SemanticizeOrderByQuery(item.OrderBy, service, entityType) 400 if err != nil { 401 return err 402 } 403 404 } else { 405 return BadRequestError("Entity type " + entity.Name + " has no navigational property " + target.Value) 406 } 407 408 return nil 409 }