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  }