mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-04 11:19:39 -06:00
Bumps [github.com/CiscoM31/godata](https://github.com/CiscoM31/godata) from 1.0.9 to 1.0.10. - [Release notes](https://github.com/CiscoM31/godata/releases) - [Commits](https://github.com/CiscoM31/godata/compare/v1.0.9...v1.0.10) --- updated-dependencies: - dependency-name: github.com/CiscoM31/godata dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
410 lines
9.3 KiB
Go
410 lines
9.3 KiB
Go
package godata
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
)
|
|
|
|
type ExpandTokenType int
|
|
|
|
func (e ExpandTokenType) Value() int {
|
|
return (int)(e)
|
|
}
|
|
|
|
const (
|
|
ExpandTokenOpenParen ExpandTokenType = iota
|
|
ExpandTokenCloseParen
|
|
ExpandTokenNav
|
|
ExpandTokenComma
|
|
ExpandTokenSemicolon
|
|
ExpandTokenEquals
|
|
ExpandTokenLiteral
|
|
)
|
|
|
|
var GlobalExpandTokenizer = ExpandTokenizer()
|
|
|
|
// Represents an item to expand in an OData query. Tracks the path of the entity
|
|
// to expand and also the filter, levels, and reference options, etc.
|
|
type ExpandItem struct {
|
|
Path []*Token
|
|
Filter *GoDataFilterQuery
|
|
At *GoDataFilterQuery
|
|
Search *GoDataSearchQuery
|
|
OrderBy *GoDataOrderByQuery
|
|
Skip *GoDataSkipQuery
|
|
Top *GoDataTopQuery
|
|
Select *GoDataSelectQuery
|
|
Compute *GoDataComputeQuery
|
|
Expand *GoDataExpandQuery
|
|
Levels int
|
|
}
|
|
|
|
func ExpandTokenizer() *Tokenizer {
|
|
t := Tokenizer{}
|
|
t.Add("^\\(", ExpandTokenOpenParen)
|
|
t.Add("^\\)", ExpandTokenCloseParen)
|
|
t.Add("^/", ExpandTokenNav)
|
|
t.Add("^,", ExpandTokenComma)
|
|
t.Add("^;", ExpandTokenSemicolon)
|
|
t.Add("^=", ExpandTokenEquals)
|
|
t.Add("^[a-zA-Z0-9_\\'\\.:\\$ \\*]+", ExpandTokenLiteral)
|
|
|
|
return &t
|
|
}
|
|
|
|
func ParseExpandString(ctx context.Context, expand string) (*GoDataExpandQuery, error) {
|
|
tokens, err := GlobalExpandTokenizer.Tokenize(ctx, expand)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stack := tokenStack{}
|
|
queue := tokenQueue{}
|
|
items := make([]*ExpandItem, 0)
|
|
|
|
for len(tokens) > 0 {
|
|
token := tokens[0]
|
|
tokens = tokens[1:]
|
|
|
|
if token.Value == "(" {
|
|
queue.Enqueue(token)
|
|
stack.Push(token)
|
|
} else if token.Value == ")" {
|
|
queue.Enqueue(token)
|
|
stack.Pop()
|
|
} else if token.Value == "," {
|
|
if stack.Empty() {
|
|
// no paren on the stack, parse this item and start a new queue
|
|
item, err := ParseExpandItem(ctx, queue)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, item)
|
|
queue = tokenQueue{}
|
|
} else {
|
|
// this comma is inside a nested expression, keep it in the queue
|
|
queue.Enqueue(token)
|
|
}
|
|
} else {
|
|
queue.Enqueue(token)
|
|
}
|
|
}
|
|
|
|
if !stack.Empty() {
|
|
return nil, BadRequestError("Mismatched parentheses in expand clause.")
|
|
}
|
|
|
|
item, err := ParseExpandItem(ctx, queue)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, item)
|
|
|
|
return &GoDataExpandQuery{ExpandItems: items}, nil
|
|
}
|
|
|
|
func ParseExpandItem(ctx context.Context, input tokenQueue) (*ExpandItem, error) {
|
|
|
|
item := &ExpandItem{}
|
|
item.Path = []*Token{}
|
|
|
|
stack := &tokenStack{}
|
|
queue := &tokenQueue{}
|
|
|
|
for !input.Empty() {
|
|
token := input.Dequeue()
|
|
if token.Value == "(" {
|
|
if !stack.Empty() {
|
|
// this is a nested slash, it belongs on the queue
|
|
queue.Enqueue(token)
|
|
} else {
|
|
// top level slash means we're done parsing the path
|
|
item.Path = append(item.Path, queue.Dequeue())
|
|
}
|
|
stack.Push(token)
|
|
} else if token.Value == ")" {
|
|
stack.Pop()
|
|
if !stack.Empty() {
|
|
// this is a nested slash, it belongs on the queue
|
|
queue.Enqueue(token)
|
|
} else {
|
|
// top level slash means we're done parsing the options
|
|
err := ParseExpandOption(ctx, queue, item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// reset the queue
|
|
queue = &tokenQueue{}
|
|
}
|
|
} else if token.Value == "/" && stack.Empty() {
|
|
if queue.Empty() {
|
|
// Disallow extra leading and intermediate slash, like /Product and Product//Info
|
|
return nil, BadRequestError("Empty path segment in expand clause.")
|
|
}
|
|
if input.Empty() {
|
|
// Disallow extra trailing slash, like Product/
|
|
return nil, BadRequestError("Empty path segment in expand clause.")
|
|
}
|
|
// at root level, slashes separate path segments
|
|
item.Path = append(item.Path, queue.Dequeue())
|
|
} else if token.Value == ";" && stack.Size == 1 {
|
|
// semicolons only split expand options at the first level
|
|
err := ParseExpandOption(ctx, queue, item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// reset the queue
|
|
queue = &tokenQueue{}
|
|
} else {
|
|
queue.Enqueue(token)
|
|
}
|
|
}
|
|
|
|
if !stack.Empty() {
|
|
return nil, BadRequestError("Mismatched parentheses in expand clause.")
|
|
}
|
|
|
|
if !queue.Empty() {
|
|
item.Path = append(item.Path, queue.Dequeue())
|
|
}
|
|
|
|
cfg, hasComplianceConfig := ctx.Value(odataCompliance).(OdataComplianceConfig)
|
|
if !hasComplianceConfig {
|
|
// Strict ODATA compliance by default.
|
|
cfg = ComplianceStrict
|
|
}
|
|
|
|
if len(item.Path) == 0 && cfg&ComplianceIgnoreInvalidComma == 0 {
|
|
return nil, BadRequestError("Extra comma in $expand.")
|
|
}
|
|
|
|
return item, nil
|
|
}
|
|
|
|
func ParseExpandOption(ctx context.Context, queue *tokenQueue, item *ExpandItem) error {
|
|
head := queue.Dequeue().Value
|
|
if queue.Head == nil {
|
|
return BadRequestError("Invalid expand clause.")
|
|
}
|
|
queue.Dequeue() // drop the '=' from the front of the queue
|
|
body := queue.GetValue()
|
|
|
|
cfg, hasComplianceConfig := ctx.Value(odataCompliance).(OdataComplianceConfig)
|
|
if !hasComplianceConfig {
|
|
// Strict ODATA compliance by default.
|
|
cfg = ComplianceStrict
|
|
}
|
|
|
|
if cfg == ComplianceStrict {
|
|
// Enforce that only supported keywords are specified in expand.
|
|
// The $levels keyword supported within expand is checked explicitly in addition to
|
|
// keywords listed in supportedOdataKeywords[] which are permitted within expand and
|
|
// at the top level of the odata query.
|
|
if _, ok := supportedOdataKeywords[head]; !ok && head != "$levels" {
|
|
return BadRequestError(fmt.Sprintf("Unsupported item '%s' in expand clause.", head))
|
|
}
|
|
}
|
|
|
|
if head == "$filter" {
|
|
filter, err := ParseFilterString(ctx, body)
|
|
if err == nil {
|
|
item.Filter = filter
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if head == "at" {
|
|
at, err := ParseFilterString(ctx, body)
|
|
if err == nil {
|
|
item.At = at
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if head == "$search" {
|
|
search, err := ParseSearchString(ctx, body)
|
|
if err == nil {
|
|
item.Search = search
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if head == "$orderby" {
|
|
orderby, err := ParseOrderByString(ctx, body)
|
|
if err == nil {
|
|
item.OrderBy = orderby
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if head == "$skip" {
|
|
skip, err := ParseSkipString(ctx, body)
|
|
if err == nil {
|
|
item.Skip = skip
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if head == "$top" {
|
|
top, err := ParseTopString(ctx, body)
|
|
if err == nil {
|
|
item.Top = top
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if head == "$select" {
|
|
sel, err := ParseSelectString(ctx, body)
|
|
if err == nil {
|
|
item.Select = sel
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if head == "$compute" {
|
|
comp, err := ParseComputeString(ctx, body)
|
|
if err == nil {
|
|
item.Compute = comp
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if head == "$expand" {
|
|
expand, err := ParseExpandString(ctx, body)
|
|
if err == nil {
|
|
item.Expand = expand
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if head == "$levels" {
|
|
i, err := strconv.Atoi(body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
item.Levels = i
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func SemanticizeExpandQuery(
|
|
expand *GoDataExpandQuery,
|
|
service *GoDataService,
|
|
entity *GoDataEntityType,
|
|
) error {
|
|
|
|
if expand == nil {
|
|
return nil
|
|
}
|
|
|
|
// Replace $levels with a nested expand clause
|
|
for _, item := range expand.ExpandItems {
|
|
if item.Levels > 0 {
|
|
if item.Expand == nil {
|
|
item.Expand = &GoDataExpandQuery{[]*ExpandItem{}}
|
|
}
|
|
// Future recursive calls to SemanticizeExpandQuery() will build out
|
|
// this expand tree completely
|
|
item.Expand.ExpandItems = append(
|
|
item.Expand.ExpandItems,
|
|
&ExpandItem{
|
|
Path: item.Path,
|
|
Levels: item.Levels - 1,
|
|
},
|
|
)
|
|
item.Levels = 0
|
|
}
|
|
}
|
|
|
|
// we're gonna rebuild the items list, replacing wildcards where possible
|
|
// TODO: can we save the garbage collector some heartache?
|
|
newItems := []*ExpandItem{}
|
|
|
|
for _, item := range expand.ExpandItems {
|
|
if item.Path[0].Value == "*" {
|
|
// replace wildcard with a copy of every navigation property
|
|
for _, navProp := range service.NavigationPropertyLookup[entity] {
|
|
path := []*Token{{Value: navProp.Name, Type: ExpandTokenLiteral}}
|
|
newItem := &ExpandItem{
|
|
Path: append(path, item.Path[1:]...),
|
|
Levels: item.Levels,
|
|
Expand: item.Expand,
|
|
}
|
|
newItems = append(newItems, newItem)
|
|
}
|
|
// TODO: check for duplicates?
|
|
} else {
|
|
newItems = append(newItems, item)
|
|
}
|
|
}
|
|
|
|
expand.ExpandItems = newItems
|
|
|
|
for _, item := range expand.ExpandItems {
|
|
err := semanticizeExpandItem(item, service, entity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func semanticizeExpandItem(
|
|
item *ExpandItem,
|
|
service *GoDataService,
|
|
entity *GoDataEntityType,
|
|
) error {
|
|
|
|
// TODO: allow multiple path segments in expand clause
|
|
// TODO: handle $ref
|
|
if len(item.Path) > 1 {
|
|
return NotImplementedError("Multiple path segments not currently supported in expand clauses.")
|
|
}
|
|
|
|
navProps := service.NavigationPropertyLookup[entity]
|
|
target := item.Path[len(item.Path)-1]
|
|
if prop, ok := navProps[target.Value]; ok {
|
|
target.SemanticType = SemanticTypeEntity
|
|
entityType, err := service.LookupEntityType(prop.Type)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
target.SemanticReference = entityType
|
|
|
|
err = SemanticizeFilterQuery(item.Filter, service, entityType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = SemanticizeExpandQuery(item.Expand, service, entityType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = SemanticizeSelectQuery(item.Select, service, entityType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = SemanticizeOrderByQuery(item.OrderBy, service, entityType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
} else {
|
|
return BadRequestError("Entity type " + entity.Name + " has no navigational property " + target.Value)
|
|
}
|
|
|
|
return nil
|
|
}
|