Initial QSfera import
This commit is contained in:
+9
@@ -0,0 +1,9 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2016 Creston Bunch
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
[](https://github.com/CiscoM31/godata/actions/workflows/go.yml)
|
||||
[](https://github.com/CiscoM31/godata/actions/workflows/golangci-lint.yml)
|
||||
|
||||
GoData
|
||||
======
|
||||
|
||||
This is an implementation of OData in Go. It is capable of parsing an OData
|
||||
request, and exposing it in a standard way so that any provider can consume
|
||||
OData requests and produce a response. Providers can be written for general
|
||||
usage like producing SQL statements for a databases, or very specific uses like
|
||||
connecting to another API.
|
||||
|
||||
Most OData server frameworks are C#/.NET or Java. These require using the CLR or
|
||||
JVM, and are overkill for a lot of use cases. By using Go we aim to provide a
|
||||
lightweight, fast, and concurrent OData service. By exposing a generic interface
|
||||
to an OData request, we hope to enable any backend to expose itself with
|
||||
an OData API with as little effort as possible.
|
||||
|
||||
Status
|
||||
======
|
||||
|
||||
This project is not finished yet, and cannot be used in its current state.
|
||||
Progress is underway to make it usable, and eventually fully compatible with the
|
||||
OData V4 specification.
|
||||
|
||||
Work in Progress
|
||||
================
|
||||
|
||||
* ~~Parse OData URLs~~
|
||||
* Create provider interface for GET requests
|
||||
* Parse OData POST and PATCH requests
|
||||
* Create provider interface for POST and PATCH requests
|
||||
* Parse OData DELETE requests
|
||||
* Create provider interface for PATCH requests
|
||||
* Allow injecting middleware into the request pipeline to enable such features
|
||||
as caching, authentication, telemetry, etc.
|
||||
* Work on fully supporting the OData specification with unit tests
|
||||
|
||||
Feel free to contribute with any of these tasks.
|
||||
|
||||
High Level Architecture
|
||||
=======================
|
||||
|
||||
If you're interesting in helping out, here is a quick introduction to the
|
||||
code to help you understand the process. The code works something like this:
|
||||
|
||||
1. A provider is initialized that defines the object model (i.e., metadata), of
|
||||
the OData service. (See the example directory.)
|
||||
2. An HTTP request is received by the request handler in service.go
|
||||
3. The URL is parsed into a data structure defined in request_model.go
|
||||
4. The request model is semanticized, so each piece of the request is associated
|
||||
with an entity/type/collection/etc. in the provider object model.
|
||||
5. The correct method and type of request (entity, collection, $metadata, $ref,
|
||||
property, etc.) is determined from the semantic information.
|
||||
6. The request is then delegated to the appropriate method of a GoDataProvider,
|
||||
which will produce a response based on the semantic information, and
|
||||
package it into a response defined in response_model.go.
|
||||
7. The response is converted to JSON and sent back to the client.
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package godata
|
||||
|
||||
import "context"
|
||||
|
||||
func ParseApplyString(ctx context.Context, apply string) (*GoDataApplyQuery, error) {
|
||||
result := GoDataApplyQuery(apply)
|
||||
return &result, nil
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package godata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The $compute query option must have a value which is a comma separated list of <expression> as <dynamic property name>
|
||||
// See https://docs.oasis-open.org/odata/odata/v4.01/os/part2-url-conventions/odata-v4.01-os-part2-url-conventions.html#sec_SystemQueryOptioncompute
|
||||
const computeAsSeparator = " as "
|
||||
|
||||
// Dynamic property names are restricted to case-insensitive a-z and the path separator /.
|
||||
var computeFieldRegex = regexp.MustCompile("^[a-zA-Z/]+$")
|
||||
|
||||
type ComputeItem struct {
|
||||
Tree *ParseNode // The compute expression parsed as a tree.
|
||||
Field string // The name of the computed dynamic property.
|
||||
}
|
||||
|
||||
// GlobalAllTokenParser is a Tokenizer which matches all tokens and ignores none. It differs from the
|
||||
// GlobalExpressionTokenizer which ignores whitespace tokens.
|
||||
var GlobalAllTokenParser *Tokenizer
|
||||
|
||||
func init() {
|
||||
t := NewExpressionParser().tokenizer
|
||||
t.TokenMatchers = append(t.IgnoreMatchers, t.TokenMatchers...)
|
||||
t.IgnoreMatchers = nil
|
||||
GlobalAllTokenParser = t
|
||||
}
|
||||
|
||||
func ParseComputeString(ctx context.Context, compute string) (*GoDataComputeQuery, error) {
|
||||
items, err := SplitComputeItems(compute)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]*ComputeItem, 0)
|
||||
fields := map[string]struct{}{}
|
||||
|
||||
for _, v := range items {
|
||||
v = strings.TrimSpace(v)
|
||||
parts := strings.Split(v, computeAsSeparator)
|
||||
if len(parts) != 2 {
|
||||
return nil, &GoDataError{
|
||||
ResponseCode: 400,
|
||||
Message: "Invalid $compute query option",
|
||||
}
|
||||
}
|
||||
field := strings.TrimSpace(parts[1])
|
||||
if !computeFieldRegex.MatchString(field) {
|
||||
return nil, &GoDataError{
|
||||
ResponseCode: 400,
|
||||
Message: "Invalid $compute query option",
|
||||
}
|
||||
}
|
||||
|
||||
if tree, err := GlobalExpressionParser.ParseExpressionString(ctx, parts[0]); err != nil {
|
||||
switch e := err.(type) {
|
||||
case *GoDataError:
|
||||
return nil, &GoDataError{
|
||||
ResponseCode: e.ResponseCode,
|
||||
Message: fmt.Sprintf("Invalid $compute query option, %s", e.Message),
|
||||
Cause: e,
|
||||
}
|
||||
default:
|
||||
return nil, &GoDataError{
|
||||
ResponseCode: 500,
|
||||
Message: "Invalid $compute query option",
|
||||
Cause: e,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if tree == nil {
|
||||
return nil, &GoDataError{
|
||||
ResponseCode: 500,
|
||||
Message: "Invalid $compute query option",
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := fields[field]; ok {
|
||||
return nil, &GoDataError{
|
||||
ResponseCode: 400,
|
||||
Message: "Invalid $compute query option",
|
||||
}
|
||||
}
|
||||
|
||||
fields[field] = struct{}{}
|
||||
|
||||
result = append(result, &ComputeItem{
|
||||
Tree: tree.Tree,
|
||||
Field: field,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &GoDataComputeQuery{result, compute}, nil
|
||||
}
|
||||
|
||||
// SplitComputeItems splits the input string based on the comma delimiter. It does so with awareness as to
|
||||
// which commas delimit $compute items and which ones are an inline part of the item, such as a separator
|
||||
// for function arguments.
|
||||
//
|
||||
// For example the input "someFunc(one,two) as three, 1 add 2 as four" results in the
|
||||
// output ["someFunc(one,two) as three", "1 add 2 as four"]
|
||||
func SplitComputeItems(in string) ([]string, error) {
|
||||
|
||||
var ret []string
|
||||
|
||||
tokens, err := GlobalAllTokenParser.Tokenize(context.Background(), in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
item := strings.Builder{}
|
||||
parenGauge := 0
|
||||
|
||||
for _, v := range tokens {
|
||||
switch v.Type {
|
||||
case ExpressionTokenOpenParen:
|
||||
parenGauge++
|
||||
case ExpressionTokenCloseParen:
|
||||
if parenGauge == 0 {
|
||||
return nil, errors.New("unmatched parentheses")
|
||||
}
|
||||
parenGauge--
|
||||
case ExpressionTokenComma:
|
||||
if parenGauge == 0 {
|
||||
ret = append(ret, item.String())
|
||||
item.Reset()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
item.WriteString(v.Value)
|
||||
}
|
||||
|
||||
if parenGauge != 0 {
|
||||
return nil, errors.New("unmatched parentheses")
|
||||
}
|
||||
|
||||
if item.Len() > 0 {
|
||||
ret = append(ret, item.String())
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package godata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func ParseCountString(ctx context.Context, count string) (*GoDataCountQuery, error) {
|
||||
i, err := strconv.ParseBool(count)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := GoDataCountQuery(i)
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package godata
|
||||
|
||||
import "fmt"
|
||||
|
||||
type GoDataError struct {
|
||||
ResponseCode int
|
||||
Message string
|
||||
Cause error
|
||||
}
|
||||
|
||||
func (err *GoDataError) Error() string {
|
||||
if err.Cause != nil {
|
||||
return fmt.Sprintf("%s. Cause: %s", err.Message, err.Cause.Error())
|
||||
}
|
||||
return err.Message
|
||||
}
|
||||
|
||||
func (err *GoDataError) Unwrap() error {
|
||||
return err.Cause
|
||||
}
|
||||
|
||||
func (err *GoDataError) SetCause(e error) *GoDataError {
|
||||
err.Cause = e
|
||||
return err
|
||||
}
|
||||
|
||||
func BadRequestError(message string) *GoDataError {
|
||||
return &GoDataError{400, message, nil}
|
||||
}
|
||||
|
||||
func NotFoundError(message string) *GoDataError {
|
||||
return &GoDataError{404, message, nil}
|
||||
}
|
||||
|
||||
func MethodNotAllowedError(message string) *GoDataError {
|
||||
return &GoDataError{405, message, nil}
|
||||
}
|
||||
|
||||
func GoneError(message string) *GoDataError {
|
||||
return &GoDataError{410, message, nil}
|
||||
}
|
||||
|
||||
func PreconditionFailedError(message string) *GoDataError {
|
||||
return &GoDataError{412, message, nil}
|
||||
}
|
||||
|
||||
func InternalServerError(message string) *GoDataError {
|
||||
return &GoDataError{500, message, nil}
|
||||
}
|
||||
|
||||
func NotImplementedError(message string) *GoDataError {
|
||||
return &GoDataError{501, message, nil}
|
||||
}
|
||||
|
||||
type UnsupportedQueryParameterError struct {
|
||||
Parameter string
|
||||
}
|
||||
|
||||
func (err *UnsupportedQueryParameterError) Error() string {
|
||||
return fmt.Sprintf("Query parameter '%s' is not supported", err.Parameter)
|
||||
}
|
||||
|
||||
type DuplicateQueryParameterError struct {
|
||||
Parameter string
|
||||
}
|
||||
|
||||
func (err *DuplicateQueryParameterError) Error() string {
|
||||
return fmt.Sprintf("Query parameter '%s' cannot be specified more than once", err.Parameter)
|
||||
}
|
||||
+409
@@ -0,0 +1,409 @@
|
||||
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
|
||||
}
|
||||
+371
@@ -0,0 +1,371 @@
|
||||
package godata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// tokenDurationRe is a regex for a token of type duration.
|
||||
// The token value is set to the ISO 8601 string inside the single quotes
|
||||
// For example, if the input data is duration'PT2H', then the token value is set to PT2H without quotes.
|
||||
const tokenDurationRe = `^(duration)?'(?P<subtoken>-?P((([0-9]+Y([0-9]+M)?([0-9]+D)?|([0-9]+M)([0-9]+D)?|([0-9]+D))(T(([0-9]+H)([0-9]+M)?([0-9]+(\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\.[0-9]+)?S)?|([0-9]+(\.[0-9]+)?S)))?)|(T(([0-9]+H)([0-9]+M)?([0-9]+(\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\.[0-9]+)?S)?|([0-9]+(\.[0-9]+)?S)))))'`
|
||||
|
||||
// Addressing properties.
|
||||
// Addressing items within a collection:
|
||||
// ABNF: entityColNavigationProperty [ collectionNavigation ]
|
||||
// collectionNavigation = [ "/" qualifiedEntityTypeName ] [ collectionNavPath ]
|
||||
// Description: OData identifier, optionally followed by collection navigation.
|
||||
//
|
||||
// propertyPath = entityColNavigationProperty [ collectionNavigation ]
|
||||
// / entityNavigationProperty [ singleNavigation ]
|
||||
// / complexColProperty [ collectionPath ]
|
||||
// / complexProperty [ complexPath ]
|
||||
// / primitiveColProperty [ collectionPath ]
|
||||
// / primitiveProperty [ singlePath ]
|
||||
// / streamProperty [ boundOperation ]
|
||||
|
||||
type ExpressionTokenType int
|
||||
|
||||
func (e ExpressionTokenType) Value() int {
|
||||
return (int)(e)
|
||||
}
|
||||
|
||||
const (
|
||||
ExpressionTokenOpenParen ExpressionTokenType = iota // Open parenthesis - parenthesis expression, list expression, or path segment selector.
|
||||
ExpressionTokenCloseParen // Close parenthesis
|
||||
ExpressionTokenWhitespace // white space token
|
||||
ExpressionTokenNav // Property navigation
|
||||
ExpressionTokenColon // Function arg separator for 'any(v:boolExpr)' and 'all(v:boolExpr)' lambda operators
|
||||
ExpressionTokenComma // [5] List delimiter and function argument delimiter.
|
||||
ExpressionTokenLogical // eq|ne|gt|ge|lt|le|and|or|not|has|in
|
||||
ExpressionTokenOp // add|sub|mul|divby|div|mod
|
||||
ExpressionTokenFunc // Function, e.g. contains, substring...
|
||||
ExpressionTokenLambdaNav // "/" token when used in lambda expression, e.g. tags/any()
|
||||
ExpressionTokenLambda // [10] any(), all() lambda functions
|
||||
ExpressionTokenCase // A case() statement. See https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_case
|
||||
ExpressionTokenCasePair // A case statement expression pair [ <boolean expression> : <value expression> ]
|
||||
ExpressionTokenNull //
|
||||
ExpressionTokenIt // The '$it' token
|
||||
ExpressionTokenRoot // [15] The '$root' token
|
||||
ExpressionTokenFloat // A floating point value.
|
||||
ExpressionTokenInteger // An integer value
|
||||
ExpressionTokenString // SQUOTE *( SQUOTE-in-string / pchar-no-SQUOTE ) SQUOTE
|
||||
ExpressionTokenDate // A date value
|
||||
ExpressionTokenTime // [20] A time value
|
||||
ExpressionTokenDateTime // A date-time value
|
||||
ExpressionTokenBoolean // A literal boolean value
|
||||
ExpressionTokenLiteral // A literal non-boolean value
|
||||
ExpressionTokenDuration // duration = [ "duration" ] SQUOTE durationValue SQUOTE
|
||||
ExpressionTokenGuid // [25] A 128-bit GUID
|
||||
ExpressionTokenAssignement // The '=' assignement for function arguments.
|
||||
ExpressionTokenGeographyPolygon // A polygon with geodetic (ie spherical) coordinates. Parsed Token.Value is '<long> <lat>,<long> <lat>...'
|
||||
ExpressionTokenGeometryPolygon // A polygon with planar (ie cartesian) coordinates. Parsed Token.Value is '<long> <lat>,<long> <lat>...'
|
||||
ExpressionTokenGeographyPoint // A geodetic coordinate point. Parsed Token.Value is '<long> <lat>'
|
||||
expressionTokenLast
|
||||
)
|
||||
|
||||
func (e ExpressionTokenType) String() string {
|
||||
return [...]string{
|
||||
"ExpressionTokenOpenParen",
|
||||
"ExpressionTokenCloseParen",
|
||||
"ExpressionTokenWhitespace",
|
||||
"ExpressionTokenNav",
|
||||
"ExpressionTokenColon",
|
||||
"ExpressionTokenComma",
|
||||
"ExpressionTokenLogical",
|
||||
"ExpressionTokenOp",
|
||||
"ExpressionTokenFunc",
|
||||
"ExpressionTokenLambdaNav",
|
||||
"ExpressionTokenLambda",
|
||||
"ExpressionTokenCase",
|
||||
"ExpressionTokenCasePair",
|
||||
"ExpressionTokenNull",
|
||||
"ExpressionTokenIt",
|
||||
"ExpressionTokenRoot",
|
||||
"ExpressionTokenFloat",
|
||||
"ExpressionTokenInteger",
|
||||
"ExpressionTokenString",
|
||||
"ExpressionTokenDate",
|
||||
"ExpressionTokenTime",
|
||||
"ExpressionTokenDateTime",
|
||||
"ExpressionTokenBoolean",
|
||||
"ExpressionTokenLiteral",
|
||||
"ExpressionTokenDuration",
|
||||
"ExpressionTokenGuid",
|
||||
"ExpressionTokenAssignement",
|
||||
"ExpressionTokenGeographyPolygon",
|
||||
"ExpressionTokenGeometryPolygon",
|
||||
"ExpressionTokenGeographyPoint",
|
||||
"expressionTokenLast",
|
||||
}[e]
|
||||
}
|
||||
|
||||
// ExpressionParser is a ODATA expression parser.
|
||||
type ExpressionParser struct {
|
||||
*Parser
|
||||
ExpectBoolExpr bool // Request expression to validate it is a boolean expression.
|
||||
tokenizer *Tokenizer // The expression tokenizer.
|
||||
}
|
||||
|
||||
// ParseExpressionString converts a ODATA expression input string into a parse
|
||||
// tree that can be used by providers to create a response.
|
||||
// Expressions can be used within $filter and $orderby query options.
|
||||
func (p *ExpressionParser) ParseExpressionString(ctx context.Context, expression string) (*GoDataExpression, error) {
|
||||
tokens, err := p.tokenizer.Tokenize(ctx, expression)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// TODO: can we do this in one fell swoop?
|
||||
postfix, err := p.InfixToPostfix(ctx, tokens)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tree, err := p.PostfixToTree(ctx, postfix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tree == nil || tree.Token == nil {
|
||||
return nil, BadRequestError("Expression cannot be nil")
|
||||
}
|
||||
if p.ExpectBoolExpr && !p.isBooleanExpression(tree.Token) {
|
||||
return nil, BadRequestError("Expression does not return a boolean value")
|
||||
}
|
||||
return &GoDataExpression{tree, expression}, nil
|
||||
}
|
||||
|
||||
var GlobalExpressionTokenizer *Tokenizer
|
||||
var GlobalExpressionParser *ExpressionParser
|
||||
|
||||
// init constructs single instances of Tokenizer and ExpressionParser and initializes their
|
||||
// respective packages variables.
|
||||
func init() {
|
||||
p := NewExpressionParser()
|
||||
t := p.tokenizer // use the Tokenizer instance created by
|
||||
|
||||
GlobalExpressionTokenizer = t
|
||||
GlobalExpressionParser = p
|
||||
|
||||
GlobalFilterTokenizer = t
|
||||
GlobalFilterParser = p
|
||||
}
|
||||
|
||||
// ExpressionTokenizer creates a tokenizer capable of tokenizing ODATA expressions.
|
||||
// 4.01 Services MUST support case-insensitive operator names.
|
||||
// See https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#_Toc31360955
|
||||
func NewExpressionTokenizer() *Tokenizer {
|
||||
t := Tokenizer{}
|
||||
// guidValue = 8HEXDIG "-" 4HEXDIG "-" 4HEXDIG "-" 4HEXDIG "-" 12HEXDIG
|
||||
t.Add(`^[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}`, ExpressionTokenGuid)
|
||||
// duration = [ "duration" ] SQUOTE durationValue SQUOTE
|
||||
// durationValue = [ SIGN ] "P" [ 1*DIGIT "D" ] [ "T" [ 1*DIGIT "H" ] [ 1*DIGIT "M" ] [ 1*DIGIT [ "." 1*DIGIT ] "S" ] ]
|
||||
// Duration literals in OData 4.0 required prefixing with “duration”.
|
||||
// In OData 4.01, services MUST support duration and enumeration literals with or without the type prefix.
|
||||
// OData clients that want to operate across OData 4.0 and OData 4.01 services should always include the prefix for duration and enumeration types.
|
||||
t.Add(tokenDurationRe, ExpressionTokenDuration)
|
||||
t.Add("^[0-9]{4,4}-[0-9]{2,2}-[0-9]{2,2}T[0-9]{2,2}:[0-9]{2,2}(:[0-9]{2,2}(.[0-9]+)?)?(Z|[+-][0-9]{2,2}:[0-9]{2,2})", ExpressionTokenDateTime)
|
||||
t.Add("^-?[0-9]{4,4}-[0-9]{2,2}-[0-9]{2,2}", ExpressionTokenDate)
|
||||
t.Add("^[0-9]{2,2}:[0-9]{2,2}(:[0-9]{2,2}(.[0-9]+)?)?", ExpressionTokenTime)
|
||||
t.Add("^\\(", ExpressionTokenOpenParen)
|
||||
t.Add("^\\)", ExpressionTokenCloseParen)
|
||||
t.Add("^(?P<token>/)(?i)(any|all)", ExpressionTokenLambdaNav) // '/' as a token between a collection expression and a lambda function any() or all()
|
||||
t.Add("^/", ExpressionTokenNav) // '/' as a token for property navigation.
|
||||
t.Add("^=", ExpressionTokenAssignement) // '=' as a token for function argument assignment.
|
||||
t.AddWithSubstituteFunc("^:", ExpressionTokenColon, func(in string) string { return "," }) // Function arg separator for lambda functions (any, all)
|
||||
t.Add("^,", ExpressionTokenComma) // Default arg separator for functions
|
||||
// Per ODATA ABNF grammar, functions must be followed by a open parenthesis.
|
||||
// This implementation is a bit more lenient and allows space character between
|
||||
// the function name and the open parenthesis.
|
||||
// TODO: If we remove the optional space character, the function token will be
|
||||
// mistakenly interpreted as a literal.
|
||||
// E.g. ABNF for 'geo.distance':
|
||||
// distanceMethodCallExpr = "geo.distance" OPEN BWS commonExpr BWS COMMA BWS commonExpr BWS CLOSE
|
||||
t.Add("(?i)^(?P<token>(geo.distance|geo.intersects|geo.length))[\\s(]", ExpressionTokenFunc)
|
||||
// Example: geography'POLYGON((-122.031577 47.578581, -122.031577 47.678581, -122.131577 47.678581))'
|
||||
t.Add(`(?i)^geography'(?:SRID=(\d{1,5});)?POLYGON\s*\(\(\s*(?P<subtoken>-?\d+(\.\d+)?\s+-?\d+(\.\d+)?(?:\s*,\s*-?\d+(\.\d+)?\s+-?\d+(\.\d+)?)*?)\s*\)\)'`, ExpressionTokenGeographyPolygon)
|
||||
t.Add(`(?i)^geometry'(?:SRID=(\d{1,5});)?POLYGON\s*\(\(\s*(?P<subtoken>-?\d+(\.\d+)?\s+-?\d+(\.\d+)?(?:\s*,\s*-?\d+(\.\d+)?\s+-?\d+(\.\d+)?)*?)\s*\)\)'`, ExpressionTokenGeometryPolygon)
|
||||
// Example: geography'POINT(-122.131577 47.678581)'
|
||||
t.Add(`(?i)^geography'POINT\s*\(\s*(?P<subtoken>-?\d+(\.\d+)?\s+-?\d+(\.\d+)?)\s*\)'`, ExpressionTokenGeographyPoint)
|
||||
// According to ODATA ABNF notation, functions must be followed by a open parenthesis with no space
|
||||
// between the function name and the open parenthesis.
|
||||
// However, we are leniently allowing space characters between the function and the open parenthesis.
|
||||
// TODO make leniency configurable.
|
||||
// E.g. ABNF for 'indexof':
|
||||
// indexOfMethodCallExpr = "indexof" OPEN BWS commonExpr BWS COMMA BWS commonExpr BWS CLOSE
|
||||
t.Add("(?i)^(?P<token>(substringof|substring|length|indexof|exists|"+
|
||||
"contains|endswith|startswith|tolower|toupper|trim|concat|year|month|day|"+
|
||||
"hour|minute|second|fractionalseconds|date|time|totaloffsetminutes|now|"+
|
||||
"maxdatetime|mindatetime|totalseconds|round|floor|ceiling|isof|cast))[\\s(]", ExpressionTokenFunc)
|
||||
// Logical operators must be followed by a space character.
|
||||
// However, in practice user have written requests such as not(City eq 'Seattle')
|
||||
// We are leniently allowing space characters between the operator name and the open parenthesis.
|
||||
// TODO make leniency configurable.
|
||||
// Example:
|
||||
// notExpr = "not" RWS boolCommonExpr
|
||||
t.Add("(?i)^(?P<token>(eq|ne|gt|ge|lt|le|and|or|not|has|in))[\\s(]", ExpressionTokenLogical)
|
||||
// Arithmetic operators must be followed by a space character.
|
||||
t.Add("(?i)^(?P<token>(add|sub|mul|divby|div|mod))\\s", ExpressionTokenOp)
|
||||
// anyExpr = "any" OPEN BWS [ lambdaVariableExpr BWS COLON BWS lambdaPredicateExpr ] BWS CLOSE
|
||||
// allExpr = "all" OPEN BWS lambdaVariableExpr BWS COLON BWS lambdaPredicateExpr BWS CLOSE
|
||||
t.Add("(?i)^(?P<token>(any|all))[\\s(]", ExpressionTokenLambda)
|
||||
t.Add("(?i)^(?P<token>(case))[\\s(]", ExpressionTokenCase)
|
||||
t.Add("^null", ExpressionTokenNull)
|
||||
t.Add("^\\$it", ExpressionTokenIt)
|
||||
t.Add("^\\$root", ExpressionTokenRoot)
|
||||
t.Add("^-?[0-9]+\\.[0-9]+", ExpressionTokenFloat)
|
||||
t.Add("^-?[0-9]+", ExpressionTokenInteger)
|
||||
t.AddWithSubstituteFunc("^'(''|[^'])*'", ExpressionTokenString, unescapeTokenString)
|
||||
t.Add("^(true|false)", ExpressionTokenBoolean)
|
||||
t.AddWithSubstituteFunc("^@*[a-zA-Z][a-zA-Z0-9_.]*",
|
||||
ExpressionTokenLiteral, unescapeUtfEncoding) // The optional '@' character is used to identify parameter aliases
|
||||
t.Ignore("^ ", ExpressionTokenWhitespace)
|
||||
|
||||
return &t
|
||||
}
|
||||
|
||||
// unescapeTokenString unescapes the input string according to the ODATA ABNF rules
|
||||
// and returns the unescaped string.
|
||||
// In ODATA ABNF, strings are encoded according to the following rules:
|
||||
// string = SQUOTE *( SQUOTE-in-string / pchar-no-SQUOTE ) SQUOTE
|
||||
// SQUOTE-in-string = SQUOTE SQUOTE ; two consecutive single quotes represent one within a string literal
|
||||
// pchar-no-SQUOTE = unreserved / pct-encoded-no-SQUOTE / other-delims / "$" / "&" / "=" / ":" / "@"
|
||||
// pct-encoded-no-SQUOTE = "%" ( "0" / "1" / "3" / "4" / "5" / "6" / "8" / "9" / A-to-F ) HEXDIG
|
||||
// / "%" "2" ( "0" / "1" / "2" / "3" / "4" / "5" / "6" / "8" / "9" / A-to-F )
|
||||
// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
||||
//
|
||||
// See http://docs.oasis-open.org/odata/odata/v4.01/csprd03/abnf/odata-abnf-construction-rules.txt
|
||||
func unescapeTokenString(in string) string {
|
||||
// The call to ReplaceAll() implements
|
||||
// SQUOTE-in-string = SQUOTE SQUOTE ; two consecutive single quotes represent one within a string literal
|
||||
if in == "''" {
|
||||
return in
|
||||
}
|
||||
return strings.ReplaceAll(in, "''", "'")
|
||||
}
|
||||
|
||||
// TODO: should we make this configurable?
|
||||
func unescapeUtfEncoding(in string) string {
|
||||
return strings.ReplaceAll(in, "_x0020_", " ")
|
||||
}
|
||||
|
||||
func NewExpressionParser() *ExpressionParser {
|
||||
parser := &ExpressionParser{
|
||||
Parser: EmptyParser().WithLiteralToken(ExpressionTokenLiteral),
|
||||
ExpectBoolExpr: false,
|
||||
tokenizer: NewExpressionTokenizer(),
|
||||
}
|
||||
parser.DefineOperator("/", 2, OpAssociationLeft, 8) // Note: '/' is used as a property navigator and between a collExpr and lambda function.
|
||||
parser.DefineOperator("has", 2, OpAssociationLeft, 8)
|
||||
// 'in' operator takes a literal list.
|
||||
// City in ('Seattle') needs to be interpreted as a list expression, not a paren expression.
|
||||
parser.DefineOperator("in", 2, OpAssociationLeft, 8).WithListExprPreference(true)
|
||||
parser.DefineOperator("-", 1, OpAssociationNone, 7)
|
||||
parser.DefineOperator("not", 1, OpAssociationRight, 7)
|
||||
parser.DefineOperator("cast", 2, OpAssociationNone, 7)
|
||||
parser.DefineOperator("mul", 2, OpAssociationNone, 6)
|
||||
parser.DefineOperator("div", 2, OpAssociationNone, 6) // Division
|
||||
parser.DefineOperator("divby", 2, OpAssociationNone, 6) // Decimal Division
|
||||
parser.DefineOperator("mod", 2, OpAssociationNone, 6)
|
||||
parser.DefineOperator("add", 2, OpAssociationNone, 5)
|
||||
parser.DefineOperator("sub", 2, OpAssociationNone, 5)
|
||||
parser.DefineOperator("gt", 2, OpAssociationLeft, 4)
|
||||
parser.DefineOperator("ge", 2, OpAssociationLeft, 4)
|
||||
parser.DefineOperator("lt", 2, OpAssociationLeft, 4)
|
||||
parser.DefineOperator("le", 2, OpAssociationLeft, 4)
|
||||
parser.DefineOperator("eq", 2, OpAssociationLeft, 3)
|
||||
parser.DefineOperator("ne", 2, OpAssociationLeft, 3)
|
||||
parser.DefineOperator("and", 2, OpAssociationLeft, 2)
|
||||
parser.DefineOperator("or", 2, OpAssociationLeft, 1)
|
||||
parser.DefineOperator("=", 2, OpAssociationRight, 0) // Function argument assignment. E.g. MyFunc(Arg1='abc')
|
||||
parser.DefineFunction("contains", []int{2}, true)
|
||||
parser.DefineFunction("endswith", []int{2}, true)
|
||||
parser.DefineFunction("startswith", []int{2}, true)
|
||||
parser.DefineFunction("exists", []int{2}, true)
|
||||
parser.DefineFunction("length", []int{1}, false)
|
||||
parser.DefineFunction("indexof", []int{2}, false)
|
||||
parser.DefineFunction("substring", []int{2, 3}, false)
|
||||
parser.DefineFunction("substringof", []int{2}, false)
|
||||
parser.DefineFunction("tolower", []int{1}, false)
|
||||
parser.DefineFunction("toupper", []int{1}, false)
|
||||
parser.DefineFunction("trim", []int{1}, false)
|
||||
parser.DefineFunction("concat", []int{2}, false)
|
||||
parser.DefineFunction("year", []int{1}, false)
|
||||
parser.DefineFunction("month", []int{1}, false)
|
||||
parser.DefineFunction("day", []int{1}, false)
|
||||
parser.DefineFunction("hour", []int{1}, false)
|
||||
parser.DefineFunction("minute", []int{1}, false)
|
||||
parser.DefineFunction("second", []int{1}, false)
|
||||
parser.DefineFunction("fractionalseconds", []int{1}, false)
|
||||
parser.DefineFunction("date", []int{1}, false)
|
||||
parser.DefineFunction("time", []int{1}, false)
|
||||
parser.DefineFunction("totaloffsetminutes", []int{1}, false)
|
||||
parser.DefineFunction("now", []int{0}, false)
|
||||
parser.DefineFunction("maxdatetime", []int{0}, false)
|
||||
parser.DefineFunction("mindatetime", []int{0}, false)
|
||||
parser.DefineFunction("totalseconds", []int{1}, false)
|
||||
parser.DefineFunction("round", []int{1}, false)
|
||||
parser.DefineFunction("floor", []int{1}, false)
|
||||
parser.DefineFunction("ceiling", []int{1}, false)
|
||||
parser.DefineFunction("isof", []int{1, 2}, true) // isof function can take one or two arguments.
|
||||
parser.DefineFunction("cast", []int{2}, false)
|
||||
parser.DefineFunction("geo.distance", []int{2}, false)
|
||||
// The geo.intersects function has the following signatures:
|
||||
// Edm.Boolean geo.intersects(Edm.GeographyPoint,Edm.GeographyPolygon)
|
||||
// Edm.Boolean geo.intersects(Edm.GeometryPoint,Edm.GeometryPolygon)
|
||||
// The geo.intersects function returns true if the specified point lies within the interior
|
||||
// or on the boundary of the specified polygon, otherwise it returns false.
|
||||
parser.DefineFunction("geo.intersects", []int{2}, true)
|
||||
// The geo.length function has the following signatures:
|
||||
// Edm.Double geo.length(Edm.GeographyLineString)
|
||||
// Edm.Double geo.length(Edm.GeometryLineString)
|
||||
// The geo.length function returns the total length of its line string parameter
|
||||
// in the coordinate reference system signified by its SRID.
|
||||
parser.DefineFunction("geo.length", []int{1}, false)
|
||||
// 'any' can take either zero or two arguments with the later having the form any(d:d/Prop eq 1).
|
||||
// Godata interprets the colon as an argument delimiter and considers the function to have two arguments.
|
||||
parser.DefineFunction("any", []int{0, 2}, true)
|
||||
// 'all' requires two arguments of a form similar to 'any'.
|
||||
parser.DefineFunction("all", []int{2}, true)
|
||||
// Define 'case' as a function accepting 1-10 arguments. Each argument is a pair of expressions separated by a colon.
|
||||
// See https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_case
|
||||
parser.DefineFunction("case", []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, true)
|
||||
|
||||
return parser
|
||||
}
|
||||
|
||||
func (p *ExpressionParser) SemanticizeExpression(
|
||||
expression *GoDataExpression,
|
||||
service *GoDataService,
|
||||
entity *GoDataEntityType,
|
||||
) error {
|
||||
|
||||
if expression == nil || expression.Tree == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var semanticizeExpressionNode func(node *ParseNode) error
|
||||
semanticizeExpressionNode = func(node *ParseNode) error {
|
||||
|
||||
if node.Token.Type == ExpressionTokenLiteral {
|
||||
prop, ok := service.PropertyLookup[entity][node.Token.Value]
|
||||
if !ok {
|
||||
return BadRequestError("No property found " + node.Token.Value + " on entity " + entity.Name)
|
||||
}
|
||||
node.Token.SemanticType = SemanticTypeProperty
|
||||
node.Token.SemanticReference = prop
|
||||
} else {
|
||||
node.Token.SemanticType = SemanticTypePropertyValue
|
||||
node.Token.SemanticReference = &node.Token.Value
|
||||
}
|
||||
|
||||
for _, child := range node.Children {
|
||||
err := semanticizeExpressionNode(child)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return semanticizeExpressionNode(expression.Tree)
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package godata
|
||||
|
||||
import "context"
|
||||
|
||||
var GlobalFilterTokenizer *Tokenizer
|
||||
var GlobalFilterParser *ExpressionParser
|
||||
|
||||
// ParseFilterString converts an input string from the $filter part of the URL into a parse
|
||||
// tree that can be used by providers to create a response.
|
||||
func ParseFilterString(ctx context.Context, filter string) (*GoDataFilterQuery, error) {
|
||||
tokens, err := GlobalFilterTokenizer.Tokenize(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// TODO: can we do this in one fell swoop?
|
||||
postfix, err := GlobalFilterParser.InfixToPostfix(ctx, tokens)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tree, err := GlobalFilterParser.PostfixToTree(ctx, postfix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tree == nil || tree.Token == nil || !GlobalFilterParser.isBooleanExpression(tree.Token) {
|
||||
return nil, BadRequestError("Value must be a boolean expression")
|
||||
}
|
||||
return &GoDataFilterQuery{tree, filter}, nil
|
||||
}
|
||||
|
||||
func SemanticizeFilterQuery(
|
||||
filter *GoDataFilterQuery,
|
||||
service *GoDataService,
|
||||
entity *GoDataEntityType,
|
||||
) error {
|
||||
|
||||
if filter == nil || filter.Tree == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var semanticizeFilterNode func(node *ParseNode) error
|
||||
semanticizeFilterNode = func(node *ParseNode) error {
|
||||
|
||||
if node.Token.Type == ExpressionTokenLiteral {
|
||||
prop, ok := service.PropertyLookup[entity][node.Token.Value]
|
||||
if !ok {
|
||||
return BadRequestError("No property found " + node.Token.Value + " on entity " + entity.Name)
|
||||
}
|
||||
node.Token.SemanticType = SemanticTypeProperty
|
||||
node.Token.SemanticReference = prop
|
||||
} else {
|
||||
node.Token.SemanticType = SemanticTypePropertyValue
|
||||
node.Token.SemanticReference = &node.Token.Value
|
||||
}
|
||||
|
||||
for _, child := range node.Children {
|
||||
err := semanticizeFilterNode(child)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return semanticizeFilterNode(filter.Tree)
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package godata
|
||||
|
||||
import "context"
|
||||
|
||||
const (
|
||||
ALLPAGES = "allpages"
|
||||
NONE = "none"
|
||||
)
|
||||
|
||||
func ParseInlineCountString(ctx context.Context, inlinecount string) (*GoDataInlineCountQuery, error) {
|
||||
result := GoDataInlineCountQuery(inlinecount)
|
||||
if inlinecount == ALLPAGES {
|
||||
return &result, nil
|
||||
} else if inlinecount == NONE {
|
||||
return &result, nil
|
||||
} else {
|
||||
return nil, BadRequestError("Invalid inlinecount query.")
|
||||
}
|
||||
}
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
package godata
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
const (
|
||||
GoDataString = "Edm.String"
|
||||
GoDataInt16 = "Edm.Int16"
|
||||
GoDataInt32 = "Edm.Int32"
|
||||
GoDataInt64 = "Edm.Int64"
|
||||
GoDataDecimal = "Edm.Decimal"
|
||||
GoDataBinary = "Edm.Binary"
|
||||
GoDataBoolean = "Edm.Boolean"
|
||||
GoDataTimeOfDay = "Edm.TimeOfDay"
|
||||
GoDataDate = "Edm.Date"
|
||||
GoDataDateTimeOffset = "Edm.DateTimeOffset"
|
||||
)
|
||||
|
||||
type GoDataMetadata struct {
|
||||
XMLName xml.Name `xml:"edmx:Edmx"`
|
||||
XMLNamespace string `xml:"xmlns:edmx,attr"`
|
||||
Version string `xml:"Version,attr"`
|
||||
DataServices *GoDataServices
|
||||
References []*GoDataReference
|
||||
}
|
||||
|
||||
func (t *GoDataMetadata) Bytes() ([]byte, error) {
|
||||
output, err := xml.MarshalIndent(t, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return append([]byte(xml.Header), output...), nil
|
||||
}
|
||||
|
||||
func (t *GoDataMetadata) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type GoDataReference struct {
|
||||
XMLName xml.Name `xml:"edmx:Reference"`
|
||||
Uri string `xml:"Uri,attr"`
|
||||
Includes []*GoDataInclude
|
||||
IncludeAnnotations []*GoDataIncludeAnnotations
|
||||
}
|
||||
|
||||
type GoDataInclude struct {
|
||||
XMLName xml.Name `xml:"edmx:Include"`
|
||||
Namespace string `xml:"Namespace,attr"`
|
||||
Alias string `xml:"Alias,attr,omitempty"`
|
||||
}
|
||||
|
||||
type GoDataIncludeAnnotations struct {
|
||||
XMLName xml.Name `xml:"edmx:IncludeAnnotations"`
|
||||
TermNamespace string `xml:"TermNamespace,attr"`
|
||||
Qualifier string `xml:"Qualifier,attr,omitempty"`
|
||||
TargetNamespace string `xml:"TargetNamespace,attr,omitempty"`
|
||||
}
|
||||
|
||||
type GoDataServices struct {
|
||||
XMLName xml.Name `xml:"edmx:DataServices"`
|
||||
Schemas []*GoDataSchema
|
||||
}
|
||||
|
||||
type GoDataSchema struct {
|
||||
XMLName xml.Name `xml:"Schema"`
|
||||
Namespace string `xml:"Namespace,attr"`
|
||||
Alias string `xml:"Alias,attr,omitempty"`
|
||||
Actions []*GoDataAction
|
||||
Annotations []*GoDataAnnotations
|
||||
Annotation []*GoDataAnnotation
|
||||
ComplexTypes []*GoDataComplexType
|
||||
EntityContainers []*GoDataEntityContainer
|
||||
EntityTypes []*GoDataEntityType
|
||||
EnumTypes []*GoDataEnumType
|
||||
Functions []*GoDataFunction
|
||||
Terms []*GoDataTerm
|
||||
TypeDefinitions []*GoDataTypeDefinition
|
||||
}
|
||||
|
||||
type GoDataAction struct {
|
||||
XMLName xml.Name `xml:"Action"`
|
||||
Name string `xml:"Name,attr"`
|
||||
IsBound string `xml:"IsBound,attr,omitempty"`
|
||||
EntitySetPath string `xml:"EntitySetPath,attr,omitempty"`
|
||||
Parameters []*GoDataParameter
|
||||
ReturnType *GoDataReturnType
|
||||
}
|
||||
|
||||
type GoDataAnnotations struct {
|
||||
XMLName xml.Name `xml:"Annotations"`
|
||||
Target string `xml:"Target,attr"`
|
||||
Qualifier string `xml:"Qualifier,attr,omitempty"`
|
||||
Annotations []*GoDataAnnotation
|
||||
}
|
||||
|
||||
type GoDataAnnotation struct {
|
||||
XMLName xml.Name `xml:"Annotation"`
|
||||
Term string `xml:"Term,attr"`
|
||||
Qualifier string `xml:"Qualifier,attr,omitempty"`
|
||||
}
|
||||
|
||||
type GoDataComplexType struct {
|
||||
XMLName xml.Name `xml:"ComplexType"`
|
||||
Name string `xml:"Name,attr"`
|
||||
BaseType string `xml:"BaseType,attr,omitempty"`
|
||||
Abstract string `xml:"Abstract,attr,omitempty"`
|
||||
OpenType string `xml:"OpenType,attr,omitempty"`
|
||||
Properties []*GoDataProperty
|
||||
NavigationProperties []*GoDataNavigationProperty
|
||||
}
|
||||
|
||||
type GoDataEntityContainer struct {
|
||||
XMLName xml.Name `xml:"EntityContainer"`
|
||||
Name string `xml:"Name,attr"`
|
||||
Extends string `xml:"Extends,attr,omitempty"`
|
||||
EntitySets []*GoDataEntitySet
|
||||
Singletons []*GoDataSingleton
|
||||
ActionImports []*GoDataActionImport
|
||||
FunctionImports []*GoDataFunctionImport
|
||||
}
|
||||
|
||||
type GoDataEntityType struct {
|
||||
XMLName xml.Name `xml:"EntityType"`
|
||||
Name string `xml:"Name,attr"`
|
||||
BaseType string `xml:"BaseType,attr,omitempty"`
|
||||
Abstract string `xml:"Abstract,attr,omitempty"`
|
||||
OpenType string `xml:"OpenType,attr,omitempty"`
|
||||
HasStream string `xml:"HasStream,attr,omitempty"`
|
||||
Key *GoDataKey
|
||||
Properties []*GoDataProperty
|
||||
NavigationProperties []*GoDataNavigationProperty
|
||||
}
|
||||
|
||||
type GoDataEnumType struct {
|
||||
XMLName xml.Name `xml:"EnumType"`
|
||||
Name string `xml:"Name,attr"`
|
||||
UnderlyingType string `xml:"UnderlyingType,attr,omitempty"`
|
||||
IsFlags string `xml:"IsFlags,attr,omitempty"`
|
||||
Members []*GoDataMember
|
||||
}
|
||||
|
||||
type GoDataFunction struct {
|
||||
XMLName xml.Name `xml:"Function"`
|
||||
Name string `xml:"Name,attr"`
|
||||
IsBound string `xml:"IsBound,attr,omitempty"`
|
||||
IsComposable string `xml:"IsComposable,attr,omitempty"`
|
||||
EntitySetPath string `xml:"EntitySetPath,attr,omitempty"`
|
||||
Parameters []*GoDataParameter
|
||||
ReturnType *GoDataReturnType
|
||||
}
|
||||
|
||||
type GoDataTypeDefinition struct {
|
||||
XMLName xml.Name `xml:"TypeDefinition"`
|
||||
Name string `xml:"Name,attr"`
|
||||
UnderlyingType string `xml:"UnderlyingTypeattr,omitempty"`
|
||||
Annotations []*GoDataAnnotation
|
||||
}
|
||||
|
||||
type GoDataProperty struct {
|
||||
XMLName xml.Name `xml:"Property"`
|
||||
Name string `xml:"Name,attr"`
|
||||
Type string `xml:"Type,attr"`
|
||||
Nullable string `xml:"Nullable,attr,omitempty"`
|
||||
MaxLength int `xml:"MaxLength,attr,omitempty"`
|
||||
Precision int `xml:"Precision,attr,omitempty"`
|
||||
Scale int `xml:"Scale,attr,omitempty"`
|
||||
Unicode string `xml:"Unicode,attr,omitempty"`
|
||||
SRID string `xml:"SRID,attr,omitempty"`
|
||||
DefaultValue string `xml:"DefaultValue,attr,omitempty"`
|
||||
}
|
||||
|
||||
type GoDataNavigationProperty struct {
|
||||
XMLName xml.Name `xml:"NavigationProperty"`
|
||||
Name string `xml:"Name,attr"`
|
||||
Type string `xml:"Type,attr"`
|
||||
Nullable string `xml:"Nullable,attr,omitempty"`
|
||||
Partner string `xml:"Partner,attr,omitempty"`
|
||||
ContainsTarget string `xml:"ContainsTarget,attr,omitempty"`
|
||||
ReferentialConstraints []*GoDataReferentialConstraint
|
||||
}
|
||||
|
||||
type GoDataReferentialConstraint struct {
|
||||
XMLName xml.Name `xml:"ReferentialConstraint"`
|
||||
Property string `xml:"Property,attr"`
|
||||
ReferencedProperty string `xml:"ReferencedProperty,attr"`
|
||||
OnDelete *GoDataOnDelete `xml:"OnDelete,omitempty"`
|
||||
}
|
||||
|
||||
type GoDataOnDelete struct {
|
||||
XMLName xml.Name `xml:"OnDelete"`
|
||||
Action string `xml:"Action,attr"`
|
||||
}
|
||||
|
||||
type GoDataEntitySet struct {
|
||||
XMLName xml.Name `xml:"EntitySet"`
|
||||
Name string `xml:"Name,attr"`
|
||||
EntityType string `xml:"EntityType,attr"`
|
||||
IncludeInServiceDocument string `xml:"IncludeInServiceDocument,attr,omitempty"`
|
||||
NavigationPropertyBindings []*GoDataNavigationPropertyBinding
|
||||
}
|
||||
|
||||
type GoDataSingleton struct {
|
||||
XMLName xml.Name `xml:"Singleton"`
|
||||
Name string `xml:"Name,attr"`
|
||||
Type string `xml:"Type,attr"`
|
||||
NavigationPropertyBindings []*GoDataNavigationPropertyBinding
|
||||
}
|
||||
|
||||
type GoDataNavigationPropertyBinding struct {
|
||||
XMLName xml.Name `xml:"NavigationPropertyBinding"`
|
||||
Path string `xml:"Path,attr"`
|
||||
Target string `xml:"Target,attr"`
|
||||
}
|
||||
|
||||
type GoDataActionImport struct {
|
||||
XMLName xml.Name `xml:"ActionImport"`
|
||||
Name string `xml:"Name,attr"`
|
||||
Action string `xml:"Action,attr"`
|
||||
EntitySet string `xml:"EntitySet,attr,omitempty"`
|
||||
}
|
||||
|
||||
type GoDataFunctionImport struct {
|
||||
XMLName xml.Name `xml:"FunctionImport"`
|
||||
Name string `xml:"Name,attr"`
|
||||
Function string `xml:"Function,attr"`
|
||||
EntitySet string `xml:"EntitySet,attr,omitempty"`
|
||||
IncludeInServiceDocument string `xml:"IncludeInServiceDocument,attr,omitempty"`
|
||||
}
|
||||
|
||||
type GoDataKey struct {
|
||||
XMLName xml.Name `xml:"Key"`
|
||||
PropertyRef *GoDataPropertyRef
|
||||
}
|
||||
|
||||
type GoDataPropertyRef struct {
|
||||
XMLName xml.Name `xml:"PropertyRef"`
|
||||
Name string `xml:"Name,attr"`
|
||||
}
|
||||
|
||||
type GoDataParameter struct {
|
||||
XMLName xml.Name `xml:"Parameter"`
|
||||
Name string `xml:"Name,attr"`
|
||||
Type string `xml:"Type,attr"`
|
||||
Nullable string `xml:"Nullable,attr,omitempty"`
|
||||
MaxLength int `xml:"MaxLength,attr,omitempty"`
|
||||
Precision int `xml:"Precision,attr,omitempty"`
|
||||
Scale int `xml:"Scale,attr,omitempty"`
|
||||
SRID string `xml:"SRID,attr,omitempty"`
|
||||
}
|
||||
|
||||
type GoDataReturnType struct {
|
||||
XMLName xml.Name `xml:"ReturnType"`
|
||||
Name string `xml:"Name,attr"`
|
||||
Type string `xml:"Type,attr"`
|
||||
Nullable string `xml:"Nullable,attr,omitempty"`
|
||||
MaxLength int `xml:"MaxLength,attr,omitempty"`
|
||||
Precision int `xml:"Precision,attr,omitempty"`
|
||||
Scale int `xml:"Scale,attr,omitempty"`
|
||||
SRID string `xml:"SRID,attr,omitempty"`
|
||||
}
|
||||
|
||||
type GoDataMember struct {
|
||||
XMLName xml.Name `xml:"Member"`
|
||||
Name string `xml:"Name,attr"`
|
||||
Value string `xml:"Value,attr,omitempty"`
|
||||
}
|
||||
|
||||
type GoDataTerm struct {
|
||||
XMLName xml.Name `xml:"Term"`
|
||||
Name string `xml:"Name,attr"`
|
||||
Type string `xml:"Type,attr"`
|
||||
BaseTerm string `xml:"BaseTerm,attr,omitempty"`
|
||||
DefaultValue string `xml:"DefaultValue,attr,omitempty"`
|
||||
AppliesTo string `xml:"AppliesTo,attr,omitempty"`
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package godata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
ASC = "asc"
|
||||
DESC = "desc"
|
||||
)
|
||||
|
||||
type OrderByItem struct {
|
||||
Field *Token // The raw value of the orderby field or expression.
|
||||
Tree *GoDataExpression // The orderby expression parsed as a tree.
|
||||
Order string // Ascending or descending order.
|
||||
}
|
||||
|
||||
func ParseOrderByString(ctx context.Context, orderby string) (*GoDataOrderByQuery, error) {
|
||||
return GlobalExpressionParser.ParseOrderByString(ctx, orderby)
|
||||
}
|
||||
|
||||
// The value of the $orderby System Query option contains a comma-separated
|
||||
// list of expressions whose primitive result values are used to sort the items.
|
||||
// The service MUST order by the specified property in ascending order.
|
||||
// 4.01 services MUST support case-insensitive values for asc and desc.
|
||||
func (p *ExpressionParser) ParseOrderByString(ctx context.Context, orderby string) (*GoDataOrderByQuery, error) {
|
||||
items := strings.Split(orderby, ",")
|
||||
|
||||
result := make([]*OrderByItem, 0)
|
||||
|
||||
for _, v := range items {
|
||||
v = strings.TrimSpace(v)
|
||||
|
||||
cfg, hasComplianceConfig := ctx.Value(odataCompliance).(OdataComplianceConfig)
|
||||
if !hasComplianceConfig {
|
||||
// Strict ODATA compliance by default.
|
||||
cfg = ComplianceStrict
|
||||
}
|
||||
|
||||
if len(v) == 0 && cfg&ComplianceIgnoreInvalidComma == 0 {
|
||||
return nil, BadRequestError("Extra comma in $orderby.")
|
||||
}
|
||||
|
||||
var order string
|
||||
vLower := strings.ToLower(v)
|
||||
if strings.HasSuffix(vLower, " "+ASC) {
|
||||
order = ASC
|
||||
} else if strings.HasSuffix(vLower, " "+DESC) {
|
||||
order = DESC
|
||||
}
|
||||
if order == "" {
|
||||
order = ASC // default order
|
||||
} else {
|
||||
v = v[:len(v)-len(order)]
|
||||
v = strings.TrimSpace(v)
|
||||
}
|
||||
|
||||
if tree, err := p.ParseExpressionString(ctx, v); err != nil {
|
||||
switch e := err.(type) {
|
||||
case *GoDataError:
|
||||
return nil, &GoDataError{
|
||||
ResponseCode: e.ResponseCode,
|
||||
Message: "Invalid $orderby query option",
|
||||
Cause: e,
|
||||
}
|
||||
default:
|
||||
return nil, &GoDataError{
|
||||
ResponseCode: 500,
|
||||
Message: "Invalid $orderby query option",
|
||||
Cause: e,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result = append(result, &OrderByItem{
|
||||
Field: &Token{Value: unescapeUtfEncoding(v)},
|
||||
Tree: tree,
|
||||
Order: order,
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return &GoDataOrderByQuery{result, orderby}, nil
|
||||
}
|
||||
|
||||
func SemanticizeOrderByQuery(orderby *GoDataOrderByQuery, service *GoDataService, entity *GoDataEntityType) error {
|
||||
if orderby == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, item := range orderby.OrderByItems {
|
||||
if prop, ok := service.PropertyLookup[entity][item.Field.Value]; ok {
|
||||
item.Field.SemanticType = SemanticTypeProperty
|
||||
item.Field.SemanticReference = prop
|
||||
} else {
|
||||
return BadRequestError("No property " + item.Field.Value + " for entity " + entity.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+916
@@ -0,0 +1,916 @@
|
||||
package godata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
OpAssociationLeft int = iota
|
||||
OpAssociationRight
|
||||
OpAssociationNone
|
||||
)
|
||||
|
||||
const (
|
||||
TokenListExpr = "list"
|
||||
|
||||
// TokenComma is the default separator for function arguments.
|
||||
TokenComma = ","
|
||||
TokenOpenParen = "("
|
||||
TokenCloseParen = ")"
|
||||
)
|
||||
|
||||
type Tokenizer struct {
|
||||
TokenMatchers []*TokenMatcher
|
||||
IgnoreMatchers []*TokenMatcher
|
||||
}
|
||||
|
||||
type TokenMatcher struct {
|
||||
Pattern string // The regular expression matching a ODATA query token, such as literal value, operator or function
|
||||
Re *regexp.Regexp // The compiled regex
|
||||
Token TokenType // The token identifier
|
||||
CaseInsensitive bool // Regex is case-insensitive
|
||||
Subst func(in string) string // A function that substitutes the raw input token with another representation. By default it is the identity.
|
||||
}
|
||||
|
||||
// TokenType is the interface that must be implemented by all token types.
|
||||
type TokenType interface {
|
||||
Value() int
|
||||
}
|
||||
|
||||
type ListExprToken int
|
||||
|
||||
func (l ListExprToken) Value() int {
|
||||
return (int)(l)
|
||||
}
|
||||
|
||||
func (l ListExprToken) String() string {
|
||||
return [...]string{
|
||||
"TokenTypeListExpr",
|
||||
"TokenTypeArgCount",
|
||||
}[l]
|
||||
}
|
||||
|
||||
const (
|
||||
// TokenTypeListExpr represents a parent node for a variadic listExpr.
|
||||
// "list"
|
||||
// "item1"
|
||||
// "item2"
|
||||
// ...
|
||||
TokenTypeListExpr ListExprToken = iota
|
||||
// TokenTypeArgCount is used to specify the number of arguments of a function or listExpr
|
||||
// This is used to handle variadic functions and listExpr.
|
||||
TokenTypeArgCount
|
||||
)
|
||||
|
||||
type Token struct {
|
||||
Value string
|
||||
Type TokenType
|
||||
// Holds information about the semantic meaning of this token taken from the
|
||||
// context of the GoDataService.
|
||||
SemanticType SemanticType
|
||||
SemanticReference interface{}
|
||||
}
|
||||
|
||||
func (t *Tokenizer) Add(pattern string, token TokenType) {
|
||||
t.AddWithSubstituteFunc(pattern, token, func(in string) string { return in })
|
||||
}
|
||||
|
||||
func (t *Tokenizer) AddWithSubstituteFunc(pattern string, token TokenType, subst func(string) string) {
|
||||
matcher := createTokenMatcher(pattern, token, subst)
|
||||
t.TokenMatchers = append(t.TokenMatchers, matcher)
|
||||
}
|
||||
|
||||
func createTokenMatcher(pattern string, token TokenType, subst func(string) string) *TokenMatcher {
|
||||
rxp := regexp.MustCompile(pattern)
|
||||
return &TokenMatcher{
|
||||
Pattern: pattern,
|
||||
Re: rxp,
|
||||
Token: token,
|
||||
CaseInsensitive: strings.Contains(pattern, "(?i)"),
|
||||
Subst: subst,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tokenizer) Ignore(pattern string, token TokenType) {
|
||||
rxp := regexp.MustCompile(pattern)
|
||||
matcher := &TokenMatcher{
|
||||
Pattern: pattern,
|
||||
Re: rxp,
|
||||
Token: token,
|
||||
CaseInsensitive: strings.Contains(pattern, "(?i)"),
|
||||
Subst: func(in string) string { return in },
|
||||
}
|
||||
t.IgnoreMatchers = append(t.IgnoreMatchers, matcher)
|
||||
}
|
||||
|
||||
// TokenizeBytes takes the input byte array and returns an array of tokens.
|
||||
// Return an empty array if there are no tokens.
|
||||
func (t *Tokenizer) TokenizeBytes(ctx context.Context, target []byte) ([]*Token, error) {
|
||||
result := make([]*Token, 0)
|
||||
match := true // false when no match is found
|
||||
for len(target) > 0 && match {
|
||||
match = false
|
||||
ignore := false
|
||||
var tokens [][]byte
|
||||
var m *TokenMatcher
|
||||
for _, m = range t.TokenMatchers {
|
||||
tokens = m.Re.FindSubmatch(target)
|
||||
if len(tokens) > 0 {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(tokens) == 0 {
|
||||
for _, m = range t.IgnoreMatchers {
|
||||
tokens = m.Re.FindSubmatch(target)
|
||||
if len(tokens) > 0 {
|
||||
ignore = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(tokens) > 0 {
|
||||
match = true
|
||||
var parsed Token
|
||||
var token []byte
|
||||
// If the regex includes a named group and the name of that group is "token"
|
||||
// then the value of the token is set to the subgroup. Other characters are
|
||||
// not consumed by the tokenization process.
|
||||
// For example, the regex:
|
||||
// ^(?P<token>(eq|ne|gt|ge|lt|le|and|or|not|has|in))\\s
|
||||
// has a group named 'token' and the group is followed by a mandatory space character.
|
||||
// If the input data is `Name eq 'Bob'`, the token is correctly set to 'eq' and
|
||||
// the space after eq is not consumed, because the space character itself is supposed
|
||||
// to be the next token.
|
||||
//
|
||||
// If Token.Value needs to be a sub-regex but the entire token needs to be consumed,
|
||||
// use 'subtoken'
|
||||
// ^(duration)?'(?P<subtoken>[0-9])'
|
||||
l := 0
|
||||
if idx := m.Re.SubexpIndex("token"); idx > 0 {
|
||||
token = tokens[idx]
|
||||
l = len(token)
|
||||
} else if idx := m.Re.SubexpIndex("subtoken"); idx > 0 {
|
||||
token = tokens[idx]
|
||||
l = len(tokens[0])
|
||||
} else {
|
||||
token = tokens[0]
|
||||
l = len(token)
|
||||
}
|
||||
target = target[l:] // remove the token from the input
|
||||
if !ignore {
|
||||
var v string
|
||||
if m.CaseInsensitive {
|
||||
// In ODATA 4.0.1, operators and functions are case insensitive.
|
||||
v = strings.ToLower(string(token))
|
||||
} else {
|
||||
v = string(token)
|
||||
}
|
||||
parsed = Token{
|
||||
Value: m.Subst(v),
|
||||
Type: m.Token,
|
||||
}
|
||||
result = append(result, &parsed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(target) > 0 && !match {
|
||||
return result, BadRequestError(fmt.Sprintf("Token '%s' is invalid", string(target)))
|
||||
}
|
||||
if len(result) < 1 {
|
||||
return result, BadRequestError("Empty query parameter")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (t *Tokenizer) Tokenize(ctx context.Context, target string) ([]*Token, error) {
|
||||
return t.TokenizeBytes(ctx, []byte(target))
|
||||
}
|
||||
|
||||
type TokenHandler func(token *Token, stack tokenStack) error
|
||||
|
||||
type Parser struct {
|
||||
// Map from string inputs to operator types
|
||||
Operators map[string]*Operator
|
||||
// Map from string inputs to function types
|
||||
Functions map[string]*Function
|
||||
|
||||
LiteralToken TokenType
|
||||
}
|
||||
|
||||
type Operator struct {
|
||||
Token string
|
||||
// Whether the operator is left/right/or not associative.
|
||||
// Determines how operators of the same precedence are grouped in the absence of parentheses.
|
||||
Association int
|
||||
// The number of operands this operator operates on
|
||||
Operands int
|
||||
// Rank of precedence. A higher value indicates higher precedence.
|
||||
Precedence int
|
||||
// Determine if the operands should be interpreted as a ListExpr or parenExpr according
|
||||
// to ODATA ABNF grammar.
|
||||
// This is only used when a listExpr has zero or one items.
|
||||
// When a listExpr has 2 or more items, there is no ambiguity between listExpr and parenExpr.
|
||||
// For example:
|
||||
// 2 + (3) ==> the right operand is a parenExpr.
|
||||
// City IN ('Seattle', 'Atlanta') ==> the right operand is unambiguously a listExpr.
|
||||
// City IN ('Seattle') ==> the right operand should be a listExpr.
|
||||
PreferListExpr bool
|
||||
}
|
||||
|
||||
func (o *Operator) WithListExprPreference(v bool) *Operator {
|
||||
o.PreferListExpr = v
|
||||
return o
|
||||
}
|
||||
|
||||
type Function struct {
|
||||
Token string // The function token
|
||||
Params []int // The number of parameters this function accepts
|
||||
ReturnsBool bool // Indicates if the function has a boolean return value
|
||||
}
|
||||
|
||||
type ParseNode struct {
|
||||
Token *Token
|
||||
Parent *ParseNode
|
||||
Children []*ParseNode
|
||||
}
|
||||
|
||||
func (p *ParseNode) String() string {
|
||||
var sb strings.Builder
|
||||
var treePrinter func(n *ParseNode, sb *strings.Builder, level int, idx *int)
|
||||
|
||||
treePrinter = func(n *ParseNode, s *strings.Builder, level int, idx *int) {
|
||||
if n == nil || n.Token == nil {
|
||||
s.WriteRune('\n')
|
||||
return
|
||||
}
|
||||
s.WriteString(fmt.Sprintf("[%2d][%2d]", *idx, n.Token.Type))
|
||||
*idx += 1
|
||||
s.WriteString(strings.Repeat(" ", level))
|
||||
s.WriteString(n.Token.Value)
|
||||
s.WriteRune('\n')
|
||||
for _, v := range n.Children {
|
||||
treePrinter(v, s, level+1, idx)
|
||||
}
|
||||
}
|
||||
idx := 0
|
||||
treePrinter(p, &sb, 0, &idx)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func EmptyParser() *Parser {
|
||||
return &Parser{
|
||||
Operators: make(map[string]*Operator),
|
||||
Functions: make(map[string]*Function),
|
||||
LiteralToken: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) WithLiteralToken(token TokenType) *Parser {
|
||||
p.LiteralToken = token
|
||||
return p
|
||||
}
|
||||
|
||||
// DefineOperator adds an operator to the language.
|
||||
// Provide the token, the expected number of arguments,
|
||||
// whether the operator is left, right, or not associative, and a precedence.
|
||||
func (p *Parser) DefineOperator(token string, operands, assoc, precedence int) *Operator {
|
||||
op := &Operator{
|
||||
Token: token,
|
||||
Association: assoc,
|
||||
Operands: operands,
|
||||
Precedence: precedence,
|
||||
}
|
||||
p.Operators[token] = op
|
||||
return op
|
||||
}
|
||||
|
||||
// DefineFunction adds a function to the language.
|
||||
// - params is the number of parameters this function accepts
|
||||
// - returnsBool indicates if the function has a boolean return value
|
||||
func (p *Parser) DefineFunction(token string, params []int, returnsBool bool) *Function {
|
||||
sort.Sort(sort.Reverse(sort.IntSlice(params)))
|
||||
f := &Function{token, params, returnsBool}
|
||||
p.Functions[token] = f
|
||||
return f
|
||||
}
|
||||
|
||||
// CustomFunctionInput serves as input to function DefineCustomFunctions()
|
||||
type CustomFunctionInput struct {
|
||||
Name string // case-insensitive function name
|
||||
NumParams []int // number of allowed parameters
|
||||
ReturnsBool bool // indicates if the function has a boolean return value
|
||||
}
|
||||
|
||||
// DefineCustomFunctions introduces additional function names to be considered as legal function
|
||||
// names while parsing. The function names must be different from all canonical functions and
|
||||
// operators defined in the odata specification.
|
||||
//
|
||||
// See https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_Functions
|
||||
func DefineCustomFunctions(functions []CustomFunctionInput) error {
|
||||
var funcNames []string
|
||||
for _, v := range functions {
|
||||
name := strings.ToLower(v.Name)
|
||||
|
||||
if GlobalExpressionParser.Functions[name] != nil {
|
||||
return fmt.Errorf("custom function '%s' may not override odata canonical function", name)
|
||||
} else if GlobalExpressionParser.Operators[name] != nil {
|
||||
return fmt.Errorf("custom function '%s' may not override odata operator", name)
|
||||
}
|
||||
|
||||
GlobalExpressionParser.DefineFunction(name, v.NumParams, v.ReturnsBool)
|
||||
funcNames = append(funcNames, name)
|
||||
}
|
||||
|
||||
// create a regex that performs a case-insensitive match of any one of the provided function names
|
||||
pattern := fmt.Sprintf("(?i)^(?P<token>(%s))[\\s(]", strings.Join(funcNames, "|"))
|
||||
matcher := createTokenMatcher(pattern, ExpressionTokenFunc, func(in string) string { return in })
|
||||
|
||||
// The tokenizer has a list of matcher expressions which are evaluated in order while parsing
|
||||
// with the first matching rule being applied. The matcher for custom functions is inserted
|
||||
// immediately following the matcher for functions defined in the Odata specification (identified
|
||||
// by finding rule with type ExpressionTokenFunc). Because the rules are applied in order based
|
||||
// on specificity, inserting at this location ensures the custom function rule has similar
|
||||
// precedence as functioned defined the Odata specification.
|
||||
list := GlobalExpressionTokenizer.TokenMatchers
|
||||
for i, v := range GlobalExpressionTokenizer.TokenMatchers {
|
||||
if v.Token == ExpressionTokenFunc {
|
||||
list = append(list[:i+1], list[i:]...)
|
||||
list[i] = matcher
|
||||
GlobalExpressionTokenizer.TokenMatchers = list
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// This is a godata package bug. The tokenizer should define matches for the token
|
||||
// type ExpressionTokenFunc for functions defined in the Odata specification.
|
||||
// Such as substring and tolower.
|
||||
return errors.New("godata parser is missing function matchers")
|
||||
}
|
||||
|
||||
func (p *Parser) isFunction(token *Token) bool {
|
||||
_, ok := p.Functions[token.Value]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (p *Parser) isOperator(token *Token) bool {
|
||||
_, ok := p.Operators[token.Value]
|
||||
return ok
|
||||
}
|
||||
|
||||
// isBooleanExpression returns True when the expression token 't' has a resulting boolean value
|
||||
func (p *Parser) isBooleanExpression(t *Token) bool {
|
||||
switch t.Type {
|
||||
case ExpressionTokenBoolean:
|
||||
// Valid boolean expression
|
||||
case ExpressionTokenLogical:
|
||||
// eq|ne|gt|ge|lt|le|and|or|not|has|in
|
||||
// Valid boolean expression
|
||||
case ExpressionTokenFunc:
|
||||
// Depends on function return type
|
||||
f := p.Functions[t.Value]
|
||||
if !f.ReturnsBool {
|
||||
return false
|
||||
}
|
||||
case ExpressionTokenLambdaNav:
|
||||
// Lambda Navigation.
|
||||
// Valid boolean expression
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// InfixToPostfix parses the input string of tokens using the given definitions of operators
|
||||
// and functions.
|
||||
// Everything else is assumed to be a literal.
|
||||
// Uses the Shunting-Yard algorithm.
|
||||
//
|
||||
// Infix notation for variadic functions and operators: f ( a, b, c, d )
|
||||
// Postfix notation with wall notation: | a b c d f
|
||||
// Postfix notation with count notation: a b c d 4 f
|
||||
func (p *Parser) InfixToPostfix(ctx context.Context, tokens []*Token) (*tokenQueue, error) {
|
||||
queue := tokenQueue{} // output queue in postfix
|
||||
stack := tokenStack{} // Operator stack
|
||||
|
||||
previousTokenIsLiteral := false
|
||||
var previousToken *Token = nil
|
||||
|
||||
incrementListArgCount := func(token *Token) {
|
||||
if !stack.Empty() {
|
||||
if previousToken != nil && previousToken.Value == TokenOpenParen {
|
||||
stack.Head.listArgCount++
|
||||
} else if stack.Head.Token.Value == TokenOpenParen {
|
||||
stack.Head.listArgCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
cfg, hasComplianceConfig := ctx.Value(odataCompliance).(OdataComplianceConfig)
|
||||
if !hasComplianceConfig {
|
||||
// Strict ODATA compliance by default.
|
||||
cfg = ComplianceStrict
|
||||
}
|
||||
for len(tokens) > 0 {
|
||||
token := tokens[0]
|
||||
tokens = tokens[1:]
|
||||
switch {
|
||||
case p.isFunction(token):
|
||||
previousTokenIsLiteral = false
|
||||
if len(tokens) == 0 || tokens[0].Value != TokenOpenParen {
|
||||
// A function token must be followed by open parenthesis token.
|
||||
return nil, BadRequestError(fmt.Sprintf("Function '%s' must be followed by '('", token.Value))
|
||||
}
|
||||
incrementListArgCount(token)
|
||||
// push functions onto the stack
|
||||
stack.Push(token)
|
||||
case p.isOperator(token):
|
||||
previousTokenIsLiteral = false
|
||||
// push operators onto stack according to precedence
|
||||
o1 := p.Operators[token.Value]
|
||||
if !stack.Empty() {
|
||||
for o2, ok := p.Operators[stack.Peek().Value]; ok &&
|
||||
((o1.Association == OpAssociationLeft && o1.Precedence <= o2.Precedence) ||
|
||||
(o1.Association == OpAssociationRight && o1.Precedence < o2.Precedence)); {
|
||||
queue.Enqueue(stack.Pop())
|
||||
|
||||
if stack.Empty() {
|
||||
break
|
||||
}
|
||||
o2, ok = p.Operators[stack.Peek().Value]
|
||||
}
|
||||
}
|
||||
if o1.Operands == 1 { // not, -
|
||||
incrementListArgCount(token)
|
||||
}
|
||||
stack.Push(token)
|
||||
case token.Value == TokenOpenParen:
|
||||
previousTokenIsLiteral = false
|
||||
// In OData, the parenthesis tokens can be used:
|
||||
// - As a parenExpr to set explicit precedence order, such as "(a + 2) x b"
|
||||
// These precedence tokens are removed while parsing the OData query.
|
||||
// - As a listExpr for multi-value sets, such as "City in ('San Jose', 'Chicago', 'Dallas')"
|
||||
// The list tokens are retained while parsing the OData query.
|
||||
// ABNF grammar:
|
||||
// listExpr = OPEN BWS commonExpr BWS *( COMMA BWS commonExpr BWS ) CLOSE
|
||||
incrementListArgCount(token)
|
||||
// Push open parens onto the stack
|
||||
stack.Push(token)
|
||||
case token.Value == TokenCloseParen:
|
||||
previousTokenIsLiteral = false
|
||||
if previousToken != nil && previousToken.Value == TokenComma {
|
||||
if cfg&ComplianceIgnoreInvalidComma == 0 {
|
||||
return nil, fmt.Errorf("invalid token sequence: %s %s", previousToken.Value, token.Value)
|
||||
}
|
||||
}
|
||||
// if we find a close paren, pop things off the stack
|
||||
for !stack.Empty() {
|
||||
if stack.Peek().Value == TokenOpenParen {
|
||||
break
|
||||
} else {
|
||||
queue.Enqueue(stack.Pop())
|
||||
}
|
||||
}
|
||||
if stack.Empty() {
|
||||
// there was an error parsing
|
||||
return nil, BadRequestError("Parse error. Mismatched parenthesis.")
|
||||
}
|
||||
|
||||
// Determine if the parenthesis delimiters are:
|
||||
// - A listExpr, possibly an empty list or single element.
|
||||
// Note a listExpr may be on the left-side or right-side of operators,
|
||||
// or it may be a list of function arguments.
|
||||
// - A parenExpr, which is used as a precedence delimiter.
|
||||
//
|
||||
// (1, 2, 3) is a listExpr, there is no ambiguity.
|
||||
// (1) matches both listExpr or parenExpr.
|
||||
// parenExpr takes precedence over listExpr.
|
||||
//
|
||||
// For example:
|
||||
// 1 IN (1, 2) ==> parenthesis is used to create a list of two elements.
|
||||
// Tags(Key='Environment')/Value ==> variadic list of arguments in property navigation.
|
||||
// (1) + (2) ==> parenthesis is a precedence delimiter, i.e. parenExpr.
|
||||
|
||||
// Get the argument count associated with the open paren.
|
||||
// Examples:
|
||||
// (a, b, c) is a listExpr with three arguments.
|
||||
// (arg1='abc',arg2=123) is a listExpr with two arguments.
|
||||
argCount := stack.getArgCount()
|
||||
// pop off open paren
|
||||
stack.Pop()
|
||||
|
||||
isListExpr := false
|
||||
popTokenFromStack := false
|
||||
|
||||
if !stack.Empty() {
|
||||
// Peek the token at the head of the stack.
|
||||
if _, ok := p.Functions[stack.Peek().Value]; ok {
|
||||
// The token is a function followed by a parenthesized expression.
|
||||
// e.g. `func(a1, a2, a3)`
|
||||
// ==> The parenthesized expression is a list expression.
|
||||
popTokenFromStack = true
|
||||
isListExpr = true
|
||||
} else if o1, ok := p.Operators[stack.Peek().Value]; ok {
|
||||
// The token is an operator followed by a parenthesized expression.
|
||||
if o1.PreferListExpr {
|
||||
// The expression is the right operand of an operator that has a preference for listExpr vs parenExpr.
|
||||
isListExpr = true
|
||||
}
|
||||
} else {
|
||||
if stack.Peek().Type == p.LiteralToken {
|
||||
// The token is a odata identifier followed by a parenthesized expression.
|
||||
// E.g. `Product(a1=abc)`:
|
||||
// ==> The parenthesized expression is a list expression.
|
||||
isListExpr = true
|
||||
popTokenFromStack = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if argCount > 1 {
|
||||
isListExpr = true
|
||||
}
|
||||
// When a listExpr contains a single item, it is ambiguous whether it is a listExpr or parenExpr.
|
||||
// For example:
|
||||
// (1) add (2) ==> there is no list involved. There are superfluous parenthesis.
|
||||
// (1, 2) in ( ('a', 'b', 'c'), (1, 2) ):
|
||||
// This is true because the LHS list (1, 2) is contained in the RHS list.
|
||||
// The following expressions are not the same:
|
||||
// (1) in ( ('a', 'b', 'c'), (2), 1 )
|
||||
// ==> false because the LHS does not contain the LHS list (1).
|
||||
// or should (1) be simplified to the integer 1, which is contained in the RHS?
|
||||
// 1 in ( ('a', 'b', 'c'), (2), 1 )
|
||||
// ==> true. The number 1 is contained in the RHS list.
|
||||
if isListExpr {
|
||||
// The open parenthesis was a delimiter for a listExpr.
|
||||
// Add a token indicating the number of arguments in the list.
|
||||
queue.Enqueue(&Token{
|
||||
Value: strconv.Itoa(argCount),
|
||||
Type: TokenTypeArgCount,
|
||||
})
|
||||
// Enqueue a 'list' token if we are processing a ListExpr.
|
||||
queue.Enqueue(&Token{
|
||||
Value: TokenListExpr,
|
||||
Type: TokenTypeListExpr,
|
||||
})
|
||||
}
|
||||
// if next token is a function or nav collection segment, move it to the queue
|
||||
if popTokenFromStack {
|
||||
queue.Enqueue(stack.Pop())
|
||||
}
|
||||
case token.Value == TokenComma:
|
||||
previousTokenIsLiteral = false
|
||||
if previousToken != nil {
|
||||
switch previousToken.Value {
|
||||
case TokenComma, TokenOpenParen:
|
||||
return nil, fmt.Errorf("invalid token sequence: %s %s", previousToken.Value, token.Value)
|
||||
}
|
||||
}
|
||||
// Function argument separator (",")
|
||||
// Comma may be used as:
|
||||
// 1. Separator of function parameters,
|
||||
// 2. Separator for listExpr such as "City IN ('Seattle', 'San Francisco')"
|
||||
//
|
||||
// Pop off stack until we see a TokenOpenParen
|
||||
for !stack.Empty() && stack.Peek().Value != TokenOpenParen {
|
||||
// This happens when the previous function argument is an expression composed
|
||||
// of multiple tokens, as opposed to a single token. For example:
|
||||
// max(sin( 5 mul pi ) add 3, sin( 5 ))
|
||||
queue.Enqueue(stack.Pop())
|
||||
}
|
||||
if stack.Empty() {
|
||||
// there was an error parsing. The top of the stack must be open parenthesis
|
||||
return nil, BadRequestError("Parse error")
|
||||
}
|
||||
if stack.Peek().Value != TokenOpenParen {
|
||||
panic("unexpected token")
|
||||
}
|
||||
|
||||
default:
|
||||
if previousTokenIsLiteral {
|
||||
return nil, fmt.Errorf("invalid token sequence: %s %s", previousToken.Value, token.Value)
|
||||
}
|
||||
if token.Type == p.LiteralToken && len(tokens) > 0 && tokens[0].Value == TokenOpenParen {
|
||||
// Literal followed by parenthesis ==> property collection navigation
|
||||
// push property segment onto the stack
|
||||
stack.Push(token)
|
||||
} else {
|
||||
// Token is a literal, number, string... -- put it in the queue
|
||||
queue.Enqueue(token)
|
||||
}
|
||||
incrementListArgCount(token)
|
||||
previousTokenIsLiteral = true
|
||||
}
|
||||
previousToken = token
|
||||
}
|
||||
|
||||
// pop off the remaining operators onto the queue
|
||||
for !stack.Empty() {
|
||||
if stack.Peek().Value == TokenOpenParen || stack.Peek().Value == TokenCloseParen {
|
||||
return nil, BadRequestError("parse error. Mismatched parenthesis.")
|
||||
}
|
||||
queue.Enqueue(stack.Pop())
|
||||
}
|
||||
return &queue, nil
|
||||
}
|
||||
|
||||
// PostfixToTree converts a Postfix token queue to a parse tree
|
||||
func (p *Parser) PostfixToTree(ctx context.Context, queue *tokenQueue) (*ParseNode, error) {
|
||||
stack := &nodeStack{}
|
||||
currNode := &ParseNode{}
|
||||
|
||||
if queue == nil {
|
||||
return nil, errors.New("input queue is nil")
|
||||
}
|
||||
t := queue.Head
|
||||
for t != nil {
|
||||
t = t.Next
|
||||
}
|
||||
// Function to process a list with a variable number of arguments.
|
||||
processVariadicArgs := func(parent *ParseNode) (int, error) {
|
||||
|
||||
// Pop off the count of list arguments.
|
||||
if stack.Empty() {
|
||||
return 0, fmt.Errorf("no argCount token found, stack is empty")
|
||||
}
|
||||
n := stack.Pop()
|
||||
if n.Token.Type != TokenTypeArgCount {
|
||||
return 0, fmt.Errorf("expected arg count token, got '%v'", n.Token.Type)
|
||||
}
|
||||
argCount, err := strconv.Atoi(n.Token.Value)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for i := 0; i < argCount; i++ {
|
||||
if stack.Empty() {
|
||||
return 0, fmt.Errorf("missing argument found. '%s'", parent.Token.Value)
|
||||
}
|
||||
c := stack.Pop()
|
||||
// Attach the operand to its parent node which represents the function/operator
|
||||
c.Parent = parent
|
||||
// prepend children so they get added in the right order
|
||||
parent.Children = append([]*ParseNode{c}, parent.Children...)
|
||||
}
|
||||
return argCount, nil
|
||||
}
|
||||
for !queue.Empty() {
|
||||
// push the token onto the stack as a tree node
|
||||
currToken := queue.Dequeue()
|
||||
currNode = &ParseNode{currToken, nil, make([]*ParseNode, 0)}
|
||||
stack.Push(currNode)
|
||||
|
||||
stackHeadToken := stack.Peek().Token
|
||||
switch {
|
||||
case p.isFunction(stackHeadToken):
|
||||
// The top of the stack is a function, pop off the function.
|
||||
node := stack.Pop()
|
||||
|
||||
// Pop off the list expression.
|
||||
if stack.Empty() {
|
||||
return nil, fmt.Errorf("no list expression token found, stack is empty")
|
||||
}
|
||||
n := stack.Pop()
|
||||
if n.Token.Type != TokenTypeListExpr {
|
||||
return nil, fmt.Errorf("expected list expression token, got '%v'", n.Token.Type)
|
||||
}
|
||||
|
||||
if node.Token.Type == ExpressionTokenCase {
|
||||
// Create argument pairs for case() statement by translating flat list into pairs
|
||||
if len(n.Children)%2 != 0 {
|
||||
return nil, fmt.Errorf("expected even number of comma-separated arguments to case statement")
|
||||
}
|
||||
for i:=0; i<len(n.Children); i+=2 {
|
||||
if !p.isBooleanExpression(n.Children[i].Token) {
|
||||
return nil, fmt.Errorf("expected boolean expression in case statement")
|
||||
}
|
||||
c := &ParseNode{
|
||||
Token: &Token{Type: ExpressionTokenCasePair},
|
||||
Parent: node,
|
||||
Children: []*ParseNode{n.Children[i],n.Children[i+1]},
|
||||
}
|
||||
node.Children = append(node.Children, c)
|
||||
}
|
||||
} else {
|
||||
// Collapse function arguments as direct children of function node
|
||||
for _, c := range n.Children {
|
||||
c.Parent = node
|
||||
}
|
||||
node.Children = n.Children
|
||||
}
|
||||
|
||||
// Some functions, e.g. substring, can take a variable number of arguments. Enforce legal number of arguments
|
||||
foundMatch := false
|
||||
f := p.Functions[node.Token.Value]
|
||||
for _, expectedArgCount := range f.Params {
|
||||
if len(node.Children) == expectedArgCount {
|
||||
foundMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundMatch {
|
||||
return nil, fmt.Errorf("invalid number of arguments for function '%s'. Got %d argument. Expected: %v",
|
||||
node.Token.Value, len(node.Children), f.Params)
|
||||
}
|
||||
stack.Push(node)
|
||||
case p.isOperator(stackHeadToken):
|
||||
// if the top of the stack is an operator
|
||||
node := stack.Pop()
|
||||
o := p.Operators[node.Token.Value]
|
||||
// pop off operands
|
||||
for i := 0; i < o.Operands; i++ {
|
||||
if stack.Empty() {
|
||||
return nil, fmt.Errorf("insufficient number of operands for operator '%s'", node.Token.Value)
|
||||
}
|
||||
// prepend children so they get added in the right order
|
||||
c := stack.Pop()
|
||||
c.Parent = node
|
||||
node.Children = append([]*ParseNode{c}, node.Children...)
|
||||
}
|
||||
stack.Push(node)
|
||||
case TokenTypeListExpr == stackHeadToken.Type:
|
||||
// ListExpr: List of items
|
||||
// Pop off the list expression.
|
||||
node := stack.Pop()
|
||||
if _, err := processVariadicArgs(node); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stack.Push(node)
|
||||
case p.LiteralToken == stackHeadToken.Type:
|
||||
// Pop off the property literal.
|
||||
node := stack.Pop()
|
||||
|
||||
if !stack.Empty() && stack.Peek().Token.Type == TokenTypeListExpr {
|
||||
// Pop off the list expression.
|
||||
n := stack.Pop()
|
||||
for _, c := range n.Children {
|
||||
c.Parent = node
|
||||
}
|
||||
node.Children = n.Children
|
||||
}
|
||||
stack.Push(node)
|
||||
}
|
||||
}
|
||||
// If all tokens have been processed, the stack should have zero or one element.
|
||||
if stack.Head != nil && stack.Head.Prev != nil {
|
||||
return nil, errors.New("invalid expression")
|
||||
}
|
||||
return currNode, nil
|
||||
}
|
||||
|
||||
type tokenStack struct {
|
||||
Head *tokenStackNode
|
||||
Size int
|
||||
}
|
||||
|
||||
type tokenStackNode struct {
|
||||
Token *Token // The token value.
|
||||
Prev *tokenStackNode // The previous node in the stack.
|
||||
listArgCount int // The number of arguments in a listExpr.
|
||||
}
|
||||
|
||||
func (s *tokenStack) Push(t *Token) {
|
||||
node := tokenStackNode{Token: t, Prev: s.Head}
|
||||
s.Head = &node
|
||||
s.Size++
|
||||
}
|
||||
|
||||
func (s *tokenStack) Pop() *Token {
|
||||
node := s.Head
|
||||
s.Head = node.Prev
|
||||
s.Size--
|
||||
return node.Token
|
||||
}
|
||||
|
||||
// Peek returns the token at head of the stack.
|
||||
// The stack is not modified.
|
||||
// A panic occurs if the stack is empty.
|
||||
func (s *tokenStack) Peek() *Token {
|
||||
return s.Head.Token
|
||||
}
|
||||
|
||||
func (s *tokenStack) Empty() bool {
|
||||
return s.Head == nil
|
||||
}
|
||||
|
||||
func (s *tokenStack) getArgCount() int {
|
||||
return s.Head.listArgCount
|
||||
}
|
||||
|
||||
func (s *tokenStack) String() string {
|
||||
output := ""
|
||||
currNode := s.Head
|
||||
for currNode != nil {
|
||||
output = currNode.Token.Value + " " + output
|
||||
currNode = currNode.Prev
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
type tokenQueue struct {
|
||||
Head *tokenQueueNode
|
||||
Tail *tokenQueueNode
|
||||
}
|
||||
|
||||
type tokenQueueNode struct {
|
||||
Token *Token
|
||||
Prev *tokenQueueNode
|
||||
Next *tokenQueueNode
|
||||
}
|
||||
|
||||
// Enqueue adds the specified token at the tail of the queue.
|
||||
func (q *tokenQueue) Enqueue(t *Token) {
|
||||
node := tokenQueueNode{t, q.Tail, nil}
|
||||
//fmt.Println(t.Value)
|
||||
|
||||
if q.Tail == nil {
|
||||
q.Head = &node
|
||||
} else {
|
||||
q.Tail.Next = &node
|
||||
}
|
||||
|
||||
q.Tail = &node
|
||||
}
|
||||
|
||||
// Dequeue removes the token at the head of the queue and returns the token.
|
||||
func (q *tokenQueue) Dequeue() *Token {
|
||||
node := q.Head
|
||||
if node.Next != nil {
|
||||
node.Next.Prev = nil
|
||||
}
|
||||
q.Head = node.Next
|
||||
if q.Head == nil {
|
||||
q.Tail = nil
|
||||
}
|
||||
return node.Token
|
||||
}
|
||||
|
||||
func (q *tokenQueue) Empty() bool {
|
||||
return q.Head == nil && q.Tail == nil
|
||||
}
|
||||
|
||||
func (q *tokenQueue) String() string {
|
||||
var sb strings.Builder
|
||||
node := q.Head
|
||||
for node != nil {
|
||||
sb.WriteString(fmt.Sprintf("%s[%v]", node.Token.Value, node.Token.Type))
|
||||
node = node.Next
|
||||
if node != nil {
|
||||
sb.WriteRune(' ')
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (q *tokenQueue) GetValue() string {
|
||||
var sb strings.Builder
|
||||
node := q.Head
|
||||
for node != nil {
|
||||
sb.WriteString(node.Token.Value)
|
||||
node = node.Next
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
type nodeStack struct {
|
||||
Head *nodeStackNode
|
||||
}
|
||||
|
||||
type nodeStackNode struct {
|
||||
ParseNode *ParseNode
|
||||
Prev *nodeStackNode
|
||||
}
|
||||
|
||||
func (s *nodeStack) Push(n *ParseNode) {
|
||||
node := nodeStackNode{ParseNode: n, Prev: s.Head}
|
||||
s.Head = &node
|
||||
}
|
||||
|
||||
func (s *nodeStack) Pop() *ParseNode {
|
||||
node := s.Head
|
||||
s.Head = node.Prev
|
||||
return node.ParseNode
|
||||
}
|
||||
|
||||
func (s *nodeStack) Peek() *ParseNode {
|
||||
return s.Head.ParseNode
|
||||
}
|
||||
|
||||
func (s *nodeStack) Empty() bool {
|
||||
return s.Head == nil
|
||||
}
|
||||
|
||||
func (s *nodeStack) String() string {
|
||||
var sb strings.Builder
|
||||
currNode := s.Head
|
||||
for currNode != nil {
|
||||
sb.WriteRune(' ')
|
||||
sb.WriteString(currNode.ParseNode.Token.Value)
|
||||
currNode = currNode.Prev
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
package godata
|
||||
|
||||
type GoDataIdentifier map[string]string
|
||||
|
||||
type RequestKind int
|
||||
|
||||
const (
|
||||
RequestKindUnknown RequestKind = iota
|
||||
RequestKindMetadata
|
||||
RequestKindService
|
||||
RequestKindEntity
|
||||
RequestKindCollection
|
||||
RequestKindSingleton
|
||||
RequestKindProperty
|
||||
RequestKindPropertyValue
|
||||
RequestKindRef
|
||||
RequestKindCount
|
||||
)
|
||||
|
||||
type SemanticType int
|
||||
|
||||
const (
|
||||
SemanticTypeUnknown SemanticType = iota
|
||||
SemanticTypeEntity
|
||||
SemanticTypeEntitySet
|
||||
SemanticTypeDerivedEntity
|
||||
SemanticTypeAction
|
||||
SemanticTypeFunction
|
||||
SemanticTypeProperty
|
||||
SemanticTypePropertyValue
|
||||
SemanticTypeRef
|
||||
SemanticTypeCount
|
||||
SemanticTypeMetadata
|
||||
)
|
||||
|
||||
type GoDataRequest struct {
|
||||
FirstSegment *GoDataSegment
|
||||
LastSegment *GoDataSegment
|
||||
Query *GoDataQuery
|
||||
RequestKind RequestKind
|
||||
}
|
||||
|
||||
// Represents a segment (slash-separated) part of the URI path. Each segment
|
||||
// has a link to the next segment (the last segment precedes nil).
|
||||
type GoDataSegment struct {
|
||||
// The raw segment parsed from the URI
|
||||
RawValue string
|
||||
|
||||
// The kind of resource being pointed at by this segment
|
||||
SemanticType SemanticType
|
||||
|
||||
// A pointer to the metadata type this object represents, as defined by a
|
||||
// particular service
|
||||
SemanticReference interface{}
|
||||
|
||||
// The name of the entity, type, collection, etc.
|
||||
Name string
|
||||
|
||||
// map[string]string of identifiers passed to this segment. If the identifier
|
||||
// is not key/value pair(s), then all values will be nil. If there is no
|
||||
// identifier, it will be nil.
|
||||
Identifier *GoDataIdentifier
|
||||
|
||||
// The next segment in the path.
|
||||
Next *GoDataSegment
|
||||
// The previous segment in the path.
|
||||
Prev *GoDataSegment
|
||||
}
|
||||
|
||||
type GoDataQuery struct {
|
||||
Filter *GoDataFilterQuery
|
||||
At *GoDataFilterQuery
|
||||
Apply *GoDataApplyQuery
|
||||
Expand *GoDataExpandQuery
|
||||
Select *GoDataSelectQuery
|
||||
OrderBy *GoDataOrderByQuery
|
||||
Top *GoDataTopQuery
|
||||
Skip *GoDataSkipQuery
|
||||
Count *GoDataCountQuery
|
||||
InlineCount *GoDataInlineCountQuery
|
||||
Search *GoDataSearchQuery
|
||||
Compute *GoDataComputeQuery
|
||||
Format *GoDataFormatQuery
|
||||
}
|
||||
|
||||
// GoDataExpression encapsulates the tree representation of an expression
|
||||
// as defined in the OData ABNF grammar.
|
||||
type GoDataExpression struct {
|
||||
Tree *ParseNode
|
||||
// The raw string representing an expression
|
||||
RawValue string
|
||||
}
|
||||
|
||||
// Stores a parsed version of the filter query string. Can be used by
|
||||
// providers to apply the filter based on their own implementation. The filter
|
||||
// is stored as a parse tree that can be traversed.
|
||||
type GoDataFilterQuery struct {
|
||||
Tree *ParseNode
|
||||
// The raw filter string
|
||||
RawValue string
|
||||
}
|
||||
|
||||
type GoDataApplyQuery string
|
||||
|
||||
type GoDataExpandQuery struct {
|
||||
ExpandItems []*ExpandItem
|
||||
}
|
||||
|
||||
type GoDataSelectQuery struct {
|
||||
SelectItems []*SelectItem
|
||||
// The raw select string
|
||||
RawValue string
|
||||
}
|
||||
|
||||
type GoDataOrderByQuery struct {
|
||||
OrderByItems []*OrderByItem
|
||||
// The raw orderby string
|
||||
RawValue string
|
||||
}
|
||||
|
||||
type GoDataTopQuery int
|
||||
|
||||
type GoDataSkipQuery int
|
||||
|
||||
type GoDataCountQuery bool
|
||||
|
||||
type GoDataInlineCountQuery string
|
||||
|
||||
type GoDataSearchQuery struct {
|
||||
Tree *ParseNode
|
||||
// The raw search string
|
||||
RawValue string
|
||||
}
|
||||
|
||||
type GoDataComputeQuery struct {
|
||||
ComputeItems []*ComputeItem
|
||||
// The raw compute string
|
||||
RawValue string
|
||||
}
|
||||
|
||||
type GoDataFormatQuery struct {
|
||||
}
|
||||
|
||||
// Check if this identifier has more than one key/value pair.
|
||||
func (id *GoDataIdentifier) HasMultiple() bool {
|
||||
count := 0
|
||||
for range map[string]string(*id) {
|
||||
count++
|
||||
}
|
||||
return count > 1
|
||||
}
|
||||
|
||||
// Return the first key in the map. This is how you should get the identifier
|
||||
// for single values, e.g. when the path is Employee(1), etc.
|
||||
func (id *GoDataIdentifier) Get() string {
|
||||
for k := range map[string]string(*id) {
|
||||
return k
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Return a specific value for a specific key.
|
||||
func (id *GoDataIdentifier) GetKey(key string) (string, bool) {
|
||||
v, ok := map[string]string(*id)[key]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// GoDataCommonStructure represents either a GoDataQuery or ExpandItem in a uniform manner
|
||||
// as a Go interface. This allows the writing of functional logic that can work on either type,
|
||||
// such as a provider implementation which starts at the GoDataQuery and walks any nested ExpandItem
|
||||
// in an identical manner.
|
||||
type GoDataCommonStructure interface {
|
||||
GetFilter() *GoDataFilterQuery
|
||||
GetAt() *GoDataFilterQuery
|
||||
GetApply() *GoDataApplyQuery
|
||||
GetExpand() *GoDataExpandQuery
|
||||
GetSelect() *GoDataSelectQuery
|
||||
GetOrderBy() *GoDataOrderByQuery
|
||||
GetTop() *GoDataTopQuery
|
||||
GetSkip() *GoDataSkipQuery
|
||||
GetCount() *GoDataCountQuery
|
||||
GetInlineCount() *GoDataInlineCountQuery
|
||||
GetSearch() *GoDataSearchQuery
|
||||
GetCompute() *GoDataComputeQuery
|
||||
GetFormat() *GoDataFormatQuery
|
||||
// AddExpandItem adds an item to the list of expand clauses in the underlying GoDataQuery/ExpandItem
|
||||
// structure.
|
||||
// AddExpandItem may be used to add items based on the requirements of the application using godata.
|
||||
// For example applications may support the introduction of dynamic navigational fields using $compute.
|
||||
// A possible implementation is to parse the request url using godata and then during semantic
|
||||
// post-processing identify dynamic navigation properties and call AddExpandItem to add them to the
|
||||
// list of expanded fields.
|
||||
AddExpandItem(*ExpandItem)
|
||||
}
|
||||
|
||||
// GoDataQuery implementation of GoDataCommonStructure interface
|
||||
func (o *GoDataQuery) GetFilter() *GoDataFilterQuery { return o.Filter }
|
||||
func (o *GoDataQuery) GetAt() *GoDataFilterQuery { return o.At }
|
||||
func (o *GoDataQuery) GetApply() *GoDataApplyQuery { return o.Apply }
|
||||
func (o *GoDataQuery) GetExpand() *GoDataExpandQuery { return o.Expand }
|
||||
func (o *GoDataQuery) GetSelect() *GoDataSelectQuery { return o.Select }
|
||||
func (o *GoDataQuery) GetOrderBy() *GoDataOrderByQuery { return o.OrderBy }
|
||||
func (o *GoDataQuery) GetTop() *GoDataTopQuery { return o.Top }
|
||||
func (o *GoDataQuery) GetSkip() *GoDataSkipQuery { return o.Skip }
|
||||
func (o *GoDataQuery) GetCount() *GoDataCountQuery { return o.Count }
|
||||
func (o *GoDataQuery) GetInlineCount() *GoDataInlineCountQuery { return o.InlineCount }
|
||||
func (o *GoDataQuery) GetSearch() *GoDataSearchQuery { return o.Search }
|
||||
func (o *GoDataQuery) GetCompute() *GoDataComputeQuery { return o.Compute }
|
||||
func (o *GoDataQuery) GetFormat() *GoDataFormatQuery { return o.Format }
|
||||
|
||||
// AddExpandItem adds an expand clause to the toplevel odata request structure 'o'.
|
||||
func (o *GoDataQuery) AddExpandItem(item *ExpandItem) {
|
||||
if o.Expand == nil {
|
||||
o.Expand = &GoDataExpandQuery{}
|
||||
}
|
||||
o.Expand.ExpandItems = append(o.Expand.ExpandItems, item)
|
||||
}
|
||||
|
||||
// ExpandItem implementation of GoDataCommonStructure interface
|
||||
func (o *ExpandItem) GetFilter() *GoDataFilterQuery { return o.Filter }
|
||||
func (o *ExpandItem) GetAt() *GoDataFilterQuery { return o.At }
|
||||
func (o *ExpandItem) GetApply() *GoDataApplyQuery { return nil }
|
||||
func (o *ExpandItem) GetExpand() *GoDataExpandQuery { return o.Expand }
|
||||
func (o *ExpandItem) GetSelect() *GoDataSelectQuery { return o.Select }
|
||||
func (o *ExpandItem) GetOrderBy() *GoDataOrderByQuery { return o.OrderBy }
|
||||
func (o *ExpandItem) GetTop() *GoDataTopQuery { return o.Top }
|
||||
func (o *ExpandItem) GetSkip() *GoDataSkipQuery { return o.Skip }
|
||||
func (o *ExpandItem) GetCount() *GoDataCountQuery { return nil }
|
||||
func (o *ExpandItem) GetInlineCount() *GoDataInlineCountQuery { return nil }
|
||||
func (o *ExpandItem) GetSearch() *GoDataSearchQuery { return o.Search }
|
||||
func (o *ExpandItem) GetCompute() *GoDataComputeQuery { return o.Compute }
|
||||
func (o *ExpandItem) GetFormat() *GoDataFormatQuery { return nil }
|
||||
|
||||
// AddExpandItem adds an expand clause to 'o' creating a nested expand, ie $expand 'item'
|
||||
// nested within $expand 'o'.
|
||||
func (o *ExpandItem) AddExpandItem(item *ExpandItem) {
|
||||
if o.Expand == nil {
|
||||
o.Expand = &GoDataExpandQuery{}
|
||||
}
|
||||
o.Expand.ExpandItems = append(o.Expand.ExpandItems, item)
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package godata
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// A response is a dictionary of keys to their corresponding fields. This will
|
||||
// be converted into a JSON dictionary in the response to the web client.
|
||||
type GoDataResponse struct {
|
||||
Fields map[string]*GoDataResponseField
|
||||
}
|
||||
|
||||
// Serialize the result as JSON for sending to the client. If an error
|
||||
// occurs during the serialization, it will be returned.
|
||||
func (r *GoDataResponse) Json() ([]byte, error) {
|
||||
result, err := prepareJsonDict(r.Fields)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// A response that is a primitive JSON type or a list or a dictionary. When
|
||||
// writing to JSON, it is automatically mapped from the Go type to a suitable
|
||||
// JSON data type. Any type can be used, but if the data type is not supported
|
||||
// for serialization, then an error is thrown.
|
||||
type GoDataResponseField struct {
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
// Convert the response field to a JSON serialized form. If the type is not
|
||||
// string, []byte, int, float64, map[string]*GoDataResponseField, or
|
||||
// []*GoDataResponseField, then an error will be thrown.
|
||||
func (f *GoDataResponseField) Json() ([]byte, error) {
|
||||
switch f.Value.(type) {
|
||||
case string:
|
||||
return prepareJsonString([]byte(f.Value.(string)))
|
||||
case []byte:
|
||||
return prepareJsonString(f.Value.([]byte))
|
||||
case int:
|
||||
return []byte(strconv.Itoa(f.Value.(int))), nil
|
||||
case float64:
|
||||
return []byte(strconv.FormatFloat(f.Value.(float64), 'f', -1, 64)), nil
|
||||
case map[string]*GoDataResponseField:
|
||||
return prepareJsonDict(f.Value.(map[string]*GoDataResponseField))
|
||||
case []*GoDataResponseField:
|
||||
return prepareJsonList(f.Value.([]*GoDataResponseField))
|
||||
default:
|
||||
return nil, InternalServerError("Response field type not recognized.")
|
||||
}
|
||||
}
|
||||
|
||||
func prepareJsonString(s []byte) ([]byte, error) {
|
||||
// escape double quotes
|
||||
s = bytes.Replace(s, []byte("\""), []byte("\\\""), -1)
|
||||
var buf bytes.Buffer
|
||||
buf.WriteByte('"')
|
||||
buf.Write(s)
|
||||
buf.WriteByte('"')
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func prepareJsonDict(d map[string]*GoDataResponseField) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteByte('{')
|
||||
count := 0
|
||||
for k, v := range d {
|
||||
buf.WriteByte('"')
|
||||
buf.Write([]byte(k))
|
||||
buf.WriteByte('"')
|
||||
buf.WriteByte(':')
|
||||
field, err := v.Json()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf.Write(field)
|
||||
count++
|
||||
if count < len(d) {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
}
|
||||
buf.WriteByte('}')
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func prepareJsonList(l []*GoDataResponseField) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteByte('[')
|
||||
count := 0
|
||||
for _, v := range l {
|
||||
field, err := v.Json()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf.Write(field)
|
||||
count++
|
||||
if count < len(l) {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
}
|
||||
buf.WriteByte(']')
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package godata
|
||||
|
||||
import "context"
|
||||
|
||||
type SearchTokenType int
|
||||
|
||||
func (s SearchTokenType) Value() int {
|
||||
return (int)(s)
|
||||
}
|
||||
|
||||
const (
|
||||
SearchTokenLiteral SearchTokenType = iota
|
||||
SearchTokenOpenParen
|
||||
SearchTokenCloseParen
|
||||
SearchTokenOp
|
||||
SearchTokenWhitespace
|
||||
)
|
||||
|
||||
var GlobalSearchTokenizer = SearchTokenizer()
|
||||
var GlobalSearchParser = SearchParser()
|
||||
|
||||
// Convert an input string from the $filter part of the URL into a parse
|
||||
// tree that can be used by providers to create a response.
|
||||
func ParseSearchString(ctx context.Context, filter string) (*GoDataSearchQuery, error) {
|
||||
tokens, err := GlobalSearchTokenizer.Tokenize(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
postfix, err := GlobalSearchParser.InfixToPostfix(ctx, tokens)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tree, err := GlobalSearchParser.PostfixToTree(ctx, postfix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &GoDataSearchQuery{tree, filter}, nil
|
||||
}
|
||||
|
||||
// Create a tokenizer capable of tokenizing filter statements
|
||||
func SearchTokenizer() *Tokenizer {
|
||||
t := Tokenizer{}
|
||||
t.Add("^\\\"[^\\\"]+\\\"", SearchTokenLiteral)
|
||||
t.Add("^\\(", SearchTokenOpenParen)
|
||||
t.Add("^\\)", SearchTokenCloseParen)
|
||||
t.Add("^(OR|AND|NOT)", SearchTokenOp)
|
||||
t.Add("^[\\w]+", SearchTokenLiteral)
|
||||
t.Ignore("^ ", SearchTokenWhitespace)
|
||||
|
||||
return &t
|
||||
}
|
||||
|
||||
func SearchParser() *Parser {
|
||||
parser := EmptyParser()
|
||||
parser.DefineOperator("NOT", 1, OpAssociationNone, 3)
|
||||
parser.DefineOperator("AND", 2, OpAssociationLeft, 2)
|
||||
parser.DefineOperator("OR", 2, OpAssociationLeft, 1)
|
||||
return parser
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package godata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SelectItem struct {
|
||||
Segments []*Token
|
||||
}
|
||||
|
||||
func ParseSelectString(ctx context.Context, sel string) (*GoDataSelectQuery, error) {
|
||||
return GlobalExpressionParser.ParseSelectString(ctx, sel)
|
||||
}
|
||||
|
||||
func (p *ExpressionParser) ParseSelectString(ctx context.Context, sel string) (*GoDataSelectQuery, error) {
|
||||
items := strings.Split(sel, ",")
|
||||
|
||||
result := []*SelectItem{}
|
||||
|
||||
for _, item := range items {
|
||||
item = strings.TrimSpace(item)
|
||||
|
||||
cfg, hasComplianceConfig := ctx.Value(odataCompliance).(OdataComplianceConfig)
|
||||
if !hasComplianceConfig {
|
||||
// Strict ODATA compliance by default.
|
||||
cfg = ComplianceStrict
|
||||
}
|
||||
|
||||
if len(strings.TrimSpace(item)) == 0 && cfg&ComplianceIgnoreInvalidComma == 0 {
|
||||
return nil, BadRequestError("Extra comma in $select.")
|
||||
}
|
||||
|
||||
if _, err := p.tokenizer.Tokenize(ctx, item); err != nil {
|
||||
switch e := err.(type) {
|
||||
case *GoDataError:
|
||||
return nil, &GoDataError{
|
||||
ResponseCode: e.ResponseCode,
|
||||
Message: "Invalid $select value",
|
||||
Cause: e,
|
||||
}
|
||||
default:
|
||||
return nil, &GoDataError{
|
||||
ResponseCode: 500,
|
||||
Message: "Invalid $select value",
|
||||
Cause: e,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
segments := []*Token{}
|
||||
for _, val := range strings.Split(item, "/") {
|
||||
segments = append(segments, &Token{Value: val})
|
||||
}
|
||||
result = append(result, &SelectItem{segments})
|
||||
}
|
||||
}
|
||||
|
||||
return &GoDataSelectQuery{result, sel}, nil
|
||||
}
|
||||
|
||||
func SemanticizeSelectQuery(sel *GoDataSelectQuery, service *GoDataService, entity *GoDataEntityType) error {
|
||||
if sel == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
newItems := []*SelectItem{}
|
||||
|
||||
// replace wildcards with every property of the entity
|
||||
for _, item := range sel.SelectItems {
|
||||
// TODO: allow multiple path segments
|
||||
if len(item.Segments) > 1 {
|
||||
return NotImplementedError("Multiple path segments in select clauses are not yet supported.")
|
||||
}
|
||||
|
||||
if item.Segments[0].Value == "*" {
|
||||
for _, prop := range service.PropertyLookup[entity] {
|
||||
newItems = append(newItems, &SelectItem{[]*Token{{Value: prop.Name}}})
|
||||
}
|
||||
} else {
|
||||
newItems = append(newItems, item)
|
||||
}
|
||||
}
|
||||
|
||||
sel.SelectItems = newItems
|
||||
|
||||
for _, item := range sel.SelectItems {
|
||||
if prop, ok := service.PropertyLookup[entity][item.Segments[0].Value]; ok {
|
||||
item.Segments[0].SemanticType = SemanticTypeProperty
|
||||
item.Segments[0].SemanticReference = prop
|
||||
} else {
|
||||
return errors.New("Entity " + entity.Name + " has no property " + item.Segments[0].Value)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+442
@@ -0,0 +1,442 @@
|
||||
package godata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
ODataFieldContext string = "@odata.context"
|
||||
ODataFieldCount string = "@odata.count"
|
||||
ODataFieldValue string = "value"
|
||||
)
|
||||
|
||||
// The basic interface for a GoData provider. All providers must implement
|
||||
// these functions.
|
||||
type GoDataProvider interface {
|
||||
// Request a single entity from the provider. Should return a response field
|
||||
// that contains the value mapping properties to values for the entity.
|
||||
GetEntity(*GoDataRequest) (*GoDataResponseField, error)
|
||||
// Request a collection of entities from the provider. Should return a
|
||||
// response field that contains the value of a slice of every entity in the
|
||||
// collection filtered by the request query parameters.
|
||||
GetEntityCollection(*GoDataRequest) (*GoDataResponseField, error)
|
||||
// Request the number of entities in a collection, disregarding any filter
|
||||
// query parameters.
|
||||
GetCount(*GoDataRequest) (int, error)
|
||||
// Get the object model representation from the provider.
|
||||
GetMetadata() *GoDataMetadata
|
||||
}
|
||||
|
||||
// A GoDataService will spawn an HTTP listener, which will connect GoData
|
||||
// requests with a backend provider given to it.
|
||||
type GoDataService struct {
|
||||
// The base url for the service. Navigating to this URL will display the
|
||||
// service document.
|
||||
BaseUrl *url.URL
|
||||
// The provider for this service that is serving the data to the OData API.
|
||||
Provider GoDataProvider
|
||||
// Metadata cache taken from the provider.
|
||||
Metadata *GoDataMetadata
|
||||
// A mapping from schema names to schema references
|
||||
SchemaLookup map[string]*GoDataSchema
|
||||
// A bottom-up mapping from entity type names to schema namespaces to
|
||||
// the entity type reference
|
||||
EntityTypeLookup map[string]map[string]*GoDataEntityType
|
||||
// A bottom-up mapping from entity container names to schema namespaces to
|
||||
// the entity container reference
|
||||
EntityContainerLookup map[string]map[string]*GoDataEntityContainer
|
||||
// A bottom-up mapping from entity set names to entity collection names to
|
||||
// schema namespaces to the entity set reference
|
||||
EntitySetLookup map[string]map[string]map[string]*GoDataEntitySet
|
||||
// A lookup for entity properties if an entity type is given, lookup
|
||||
// properties by name
|
||||
PropertyLookup map[*GoDataEntityType]map[string]*GoDataProperty
|
||||
// A lookup for navigational properties if an entity type is given,
|
||||
// lookup navigational properties by name
|
||||
NavigationPropertyLookup map[*GoDataEntityType]map[string]*GoDataNavigationProperty
|
||||
}
|
||||
|
||||
type providerChannelResponse struct {
|
||||
Field *GoDataResponseField
|
||||
Error error
|
||||
}
|
||||
|
||||
// Create a new service from a given provider. This step builds lookups for
|
||||
// all parts of the data model, so constant time lookups can be performed. This
|
||||
// step only happens once when the server starts up, so the overall cost is
|
||||
// minimal. The given url will be treated as the base URL for all service
|
||||
// requests, and used for building context URLs, etc.
|
||||
func BuildService(provider GoDataProvider, serviceUrl string) (*GoDataService, error) {
|
||||
metadata := provider.GetMetadata()
|
||||
|
||||
// build the lookups from the metadata
|
||||
schemaLookup := map[string]*GoDataSchema{}
|
||||
entityLookup := map[string]map[string]*GoDataEntityType{}
|
||||
containerLookup := map[string]map[string]*GoDataEntityContainer{}
|
||||
entitySetLookup := map[string]map[string]map[string]*GoDataEntitySet{}
|
||||
propertyLookup := map[*GoDataEntityType]map[string]*GoDataProperty{}
|
||||
navPropLookup := map[*GoDataEntityType]map[string]*GoDataNavigationProperty{}
|
||||
|
||||
for _, schema := range metadata.DataServices.Schemas {
|
||||
schemaLookup[schema.Namespace] = schema
|
||||
|
||||
for _, entity := range schema.EntityTypes {
|
||||
if _, ok := entityLookup[entity.Name]; !ok {
|
||||
entityLookup[entity.Name] = map[string]*GoDataEntityType{}
|
||||
}
|
||||
if _, ok := propertyLookup[entity]; !ok {
|
||||
propertyLookup[entity] = map[string]*GoDataProperty{}
|
||||
}
|
||||
if _, ok := navPropLookup[entity]; !ok {
|
||||
navPropLookup[entity] = map[string]*GoDataNavigationProperty{}
|
||||
}
|
||||
entityLookup[entity.Name][schema.Namespace] = entity
|
||||
|
||||
for _, prop := range entity.Properties {
|
||||
propertyLookup[entity][prop.Name] = prop
|
||||
}
|
||||
for _, prop := range entity.NavigationProperties {
|
||||
navPropLookup[entity][prop.Name] = prop
|
||||
}
|
||||
}
|
||||
|
||||
for _, container := range schema.EntityContainers {
|
||||
if _, ok := containerLookup[container.Name]; !ok {
|
||||
containerLookup[container.Name] = map[string]*GoDataEntityContainer{}
|
||||
}
|
||||
containerLookup[container.Name][schema.Namespace] = container
|
||||
|
||||
for _, set := range container.EntitySets {
|
||||
if _, ok := entitySetLookup[set.Name]; !ok {
|
||||
entitySetLookup[set.Name] = map[string]map[string]*GoDataEntitySet{}
|
||||
}
|
||||
if _, ok := entitySetLookup[set.Name][container.Name]; !ok {
|
||||
entitySetLookup[set.Name][container.Name] = map[string]*GoDataEntitySet{}
|
||||
}
|
||||
entitySetLookup[set.Name][container.Name][schema.Namespace] = set
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parsedUrl, err := url.Parse(serviceUrl)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &GoDataService{
|
||||
parsedUrl,
|
||||
provider,
|
||||
provider.GetMetadata(),
|
||||
schemaLookup,
|
||||
entityLookup,
|
||||
containerLookup,
|
||||
entitySetLookup,
|
||||
propertyLookup,
|
||||
navPropLookup,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// The default handler for parsing requests as GoDataRequests, passing them
|
||||
// to a GoData provider, and then building a response.
|
||||
func (service *GoDataService) GoDataHTTPHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.Background()
|
||||
request, err := ParseRequest(ctx, r.URL.Path, r.URL.Query())
|
||||
|
||||
if err != nil {
|
||||
panic(err) // TODO: return proper error
|
||||
}
|
||||
|
||||
// Semanticize all tokens in the request, connecting them with their
|
||||
// corresponding types in the service
|
||||
err = request.SemanticizeRequest(service)
|
||||
|
||||
if err != nil {
|
||||
panic(err) // TODO: return proper error
|
||||
}
|
||||
|
||||
// TODO: differentiate GET and POST requests
|
||||
var response []byte = []byte{}
|
||||
if request.RequestKind == RequestKindMetadata {
|
||||
response, err = service.buildMetadataResponse(request)
|
||||
} else if request.RequestKind == RequestKindService {
|
||||
response, err = service.buildServiceResponse(request)
|
||||
} else if request.RequestKind == RequestKindCollection {
|
||||
response, err = service.buildCollectionResponse(request)
|
||||
} else if request.RequestKind == RequestKindEntity {
|
||||
response, err = service.buildEntityResponse(request)
|
||||
} else if request.RequestKind == RequestKindProperty {
|
||||
response, err = service.buildPropertyResponse(request)
|
||||
} else if request.RequestKind == RequestKindPropertyValue {
|
||||
response, err = service.buildPropertyValueResponse(request)
|
||||
} else if request.RequestKind == RequestKindCount {
|
||||
response, err = service.buildCountResponse(request)
|
||||
} else if request.RequestKind == RequestKindRef {
|
||||
response, err = service.buildRefResponse(request)
|
||||
} else {
|
||||
err = NotImplementedError("Request type not understood.")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
panic(err) // TODO: return proper error
|
||||
}
|
||||
|
||||
_, err = w.Write(response)
|
||||
if err != nil {
|
||||
panic(err) // TODO: return proper error
|
||||
}
|
||||
}
|
||||
|
||||
func (service *GoDataService) buildMetadataResponse(request *GoDataRequest) ([]byte, error) {
|
||||
return service.Metadata.Bytes()
|
||||
}
|
||||
|
||||
func (service *GoDataService) buildServiceResponse(request *GoDataRequest) ([]byte, error) {
|
||||
// TODO
|
||||
return nil, NotImplementedError("Service responses are not implemented yet.")
|
||||
}
|
||||
|
||||
func (service *GoDataService) buildCollectionResponse(request *GoDataRequest) ([]byte, error) {
|
||||
response := &GoDataResponse{Fields: map[string]*GoDataResponseField{}}
|
||||
// get request from provider
|
||||
responses := make(chan *providerChannelResponse)
|
||||
go func() {
|
||||
result, err := service.Provider.GetEntityCollection(request)
|
||||
responses <- &providerChannelResponse{result, err}
|
||||
close(responses)
|
||||
}()
|
||||
|
||||
if bool(*request.Query.Count) {
|
||||
// if count is true, also include the count result
|
||||
counts := make(chan *providerChannelResponse)
|
||||
|
||||
go func() {
|
||||
result, err := service.Provider.GetCount(request)
|
||||
counts <- &providerChannelResponse{&GoDataResponseField{result}, err}
|
||||
close(counts)
|
||||
}()
|
||||
|
||||
r := <-counts
|
||||
if r.Error != nil {
|
||||
return nil, r.Error
|
||||
}
|
||||
|
||||
response.Fields[ODataFieldCount] = r.Field
|
||||
}
|
||||
// build context URL
|
||||
context := request.LastSegment.SemanticReference.(*GoDataEntitySet).Name
|
||||
path, err := url.Parse("./$metadata#" + context)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
contextUrl := service.BaseUrl.ResolveReference(path).String()
|
||||
response.Fields[ODataFieldContext] = &GoDataResponseField{Value: contextUrl}
|
||||
|
||||
// wait for a response from the provider
|
||||
r := <-responses
|
||||
|
||||
if r.Error != nil {
|
||||
return nil, r.Error
|
||||
}
|
||||
|
||||
response.Fields[ODataFieldValue] = r.Field
|
||||
|
||||
return response.Json()
|
||||
}
|
||||
|
||||
func (service *GoDataService) buildEntityResponse(request *GoDataRequest) ([]byte, error) {
|
||||
// get request from provider
|
||||
responses := make(chan *providerChannelResponse)
|
||||
go func() {
|
||||
result, err := service.Provider.GetEntity(request)
|
||||
responses <- &providerChannelResponse{result, err}
|
||||
close(responses)
|
||||
}()
|
||||
|
||||
// build context URL
|
||||
context := request.LastSegment.SemanticReference.(*GoDataEntitySet).Name
|
||||
path, err := url.Parse("./$metadata#" + context + "/$entity")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
contextUrl := service.BaseUrl.ResolveReference(path).String()
|
||||
|
||||
// wait for a response from the provider
|
||||
r := <-responses
|
||||
|
||||
if r.Error != nil {
|
||||
return nil, r.Error
|
||||
}
|
||||
|
||||
// Add context field to result and create the response
|
||||
switch r.Field.Value.(type) {
|
||||
case map[string]*GoDataResponseField:
|
||||
fields := r.Field.Value.(map[string]*GoDataResponseField)
|
||||
fields[ODataFieldContext] = &GoDataResponseField{Value: contextUrl}
|
||||
response := &GoDataResponse{Fields: fields}
|
||||
|
||||
return response.Json()
|
||||
default:
|
||||
return nil, InternalServerError("Provider did not return a valid response" +
|
||||
" from GetEntity()")
|
||||
}
|
||||
}
|
||||
|
||||
func (service *GoDataService) buildPropertyResponse(request *GoDataRequest) ([]byte, error) {
|
||||
// TODO
|
||||
return nil, NotImplementedError("Property responses are not implemented yet.")
|
||||
}
|
||||
|
||||
func (service *GoDataService) buildPropertyValueResponse(request *GoDataRequest) ([]byte, error) {
|
||||
// TODO
|
||||
return nil, NotImplementedError("Property value responses are not implemented yet.")
|
||||
}
|
||||
|
||||
func (service *GoDataService) buildCountResponse(request *GoDataRequest) ([]byte, error) {
|
||||
// get request from provider
|
||||
responses := make(chan *providerChannelResponse)
|
||||
go func() {
|
||||
result, err := service.Provider.GetCount(request)
|
||||
responses <- &providerChannelResponse{&GoDataResponseField{result}, err}
|
||||
close(responses)
|
||||
}()
|
||||
|
||||
// wait for a response from the provider
|
||||
r := <-responses
|
||||
|
||||
if r.Error != nil {
|
||||
return nil, r.Error
|
||||
}
|
||||
|
||||
return r.Field.Json()
|
||||
}
|
||||
|
||||
func (service *GoDataService) buildRefResponse(request *GoDataRequest) ([]byte, error) {
|
||||
// TODO
|
||||
return nil, NotImplementedError("Ref responses are not implemented yet.")
|
||||
}
|
||||
|
||||
// Start the service listening on the given address.
|
||||
func (service *GoDataService) ListenAndServe(addr string) {
|
||||
http.HandleFunc("/", service.GoDataHTTPHandler)
|
||||
if err := http.ListenAndServe(addr, nil); err != nil {
|
||||
panic(err) // TODO: return proper error
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup an entity type from the service metadata. Accepts a fully qualified
|
||||
// name, e.g., ODataService.EntityTypeName or, if unambiguous, accepts a
|
||||
// simple identifier, e.g., EntityTypeName.
|
||||
func (service *GoDataService) LookupEntityType(name string) (*GoDataEntityType, error) {
|
||||
// strip "Collection()" and just return the raw entity type
|
||||
// The provider should keep track of what are collections and what aren't
|
||||
if strings.Contains(name, "(") && strings.Contains(name, ")") {
|
||||
name = name[strings.Index(name, "(")+1 : strings.LastIndex(name, ")")]
|
||||
}
|
||||
|
||||
parts := strings.Split(name, ".")
|
||||
entityName := parts[len(parts)-1]
|
||||
// remove entity from the list of parts
|
||||
parts = parts[:len(parts)-1]
|
||||
|
||||
schemas, ok := service.EntityTypeLookup[entityName]
|
||||
if !ok {
|
||||
return nil, BadRequestError("Entity " + name + " does not exist.")
|
||||
}
|
||||
|
||||
if len(parts) > 0 {
|
||||
// namespace is provided
|
||||
entity, ok := schemas[parts[len(parts)-1]]
|
||||
if !ok {
|
||||
return nil, BadRequestError("Entity " + name + " not found in given namespace.")
|
||||
}
|
||||
return entity, nil
|
||||
} else {
|
||||
// no namespace, just return the first one
|
||||
|
||||
// throw error if ambiguous
|
||||
if len(schemas) > 1 {
|
||||
return nil, BadRequestError("Entity " + name + " is ambiguous. Please provide a namespace.")
|
||||
}
|
||||
|
||||
for _, v := range schemas {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If this happens, that's very bad
|
||||
return nil, BadRequestError("No schema lookup found for entity " + name)
|
||||
}
|
||||
|
||||
// Lookup an entity set from the service metadata. Accepts a fully qualified
|
||||
// name, e.g., ODataService.ContainerName.EntitySetName,
|
||||
// ContainerName.EntitySetName or, if unambiguous, accepts a simple identifier,
|
||||
// e.g., EntitySetName.
|
||||
func (service *GoDataService) LookupEntitySet(name string) (*GoDataEntitySet, error) {
|
||||
parts := strings.Split(name, ".")
|
||||
setName := parts[len(parts)-1]
|
||||
// remove entity set from the list of parts
|
||||
parts = parts[:len(parts)-1]
|
||||
|
||||
containers, ok := service.EntitySetLookup[setName]
|
||||
if !ok {
|
||||
return nil, BadRequestError("Entity set " + name + " does not exist.")
|
||||
}
|
||||
|
||||
if len(parts) > 0 {
|
||||
// container is provided
|
||||
schemas, ok := containers[parts[len(parts)-1]]
|
||||
if !ok {
|
||||
return nil, BadRequestError("Container " + name + " not found.")
|
||||
}
|
||||
|
||||
// remove container name from the list of parts
|
||||
parts = parts[:len(parts)-1]
|
||||
|
||||
if len(parts) > 0 {
|
||||
// schema is provided
|
||||
set, ok := schemas[parts[len(parts)-1]]
|
||||
if !ok {
|
||||
return nil, BadRequestError("Entity set " + name + " not found.")
|
||||
}
|
||||
return set, nil
|
||||
} else {
|
||||
// no schema is provided
|
||||
|
||||
if len(schemas) > 1 {
|
||||
// container is ambiguous
|
||||
return nil, BadRequestError("Entity set " + name + " is ambiguous. Please provide fully qualified name.")
|
||||
}
|
||||
|
||||
// there should be one schema, if not then something went horribly wrong
|
||||
for _, set := range schemas {
|
||||
return set, nil
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// no container is provided
|
||||
|
||||
// return error if entity set is ambiguous
|
||||
if len(containers) > 1 {
|
||||
return nil, BadRequestError("Entity set " + name + " is ambiguous. Please provide fully qualified name.")
|
||||
}
|
||||
|
||||
// find the first schema, it will be the only one
|
||||
for _, schemas := range containers {
|
||||
if len(schemas) > 1 {
|
||||
// container is ambiguous
|
||||
return nil, BadRequestError("Entity set " + name + " is ambiguous. Please provide fully qualified name.")
|
||||
}
|
||||
|
||||
// there should be one schema, if not then something went horribly wrong
|
||||
for _, set := range schemas {
|
||||
return set, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, BadRequestError("Entity set " + name + " not found.")
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package godata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func ParseTopString(ctx context.Context, top string) (*GoDataTopQuery, error) {
|
||||
i, err := strconv.Atoi(top)
|
||||
result := GoDataTopQuery(i)
|
||||
return &result, err
|
||||
}
|
||||
|
||||
func ParseSkipString(ctx context.Context, skip string) (*GoDataSkipQuery, error) {
|
||||
i, err := strconv.Atoi(skip)
|
||||
result := GoDataSkipQuery(i)
|
||||
return &result, err
|
||||
}
|
||||
+409
@@ -0,0 +1,409 @@
|
||||
package godata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Parse a request from the HTTP server and format it into a GoDaataRequest type
|
||||
// to be passed to a provider to produce a result.
|
||||
func ParseRequest(ctx context.Context, path string, query url.Values) (*GoDataRequest, error) {
|
||||
r := &GoDataRequest{
|
||||
RequestKind: RequestKindUnknown,
|
||||
}
|
||||
|
||||
if err := r.ParseUrlPath(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := r.ParseUrlQuery(ctx, query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Compare a request to a given service, and validate the semantics and update
|
||||
// the request with semantics included
|
||||
func (req *GoDataRequest) SemanticizeRequest(service *GoDataService) error {
|
||||
|
||||
// if request kind is a resource
|
||||
for segment := req.FirstSegment; segment != nil; segment = segment.Next {
|
||||
err := SemanticizePathSegment(segment, service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
switch req.LastSegment.SemanticReference.(type) {
|
||||
case *GoDataEntitySet:
|
||||
entitySet := req.LastSegment.SemanticReference.(*GoDataEntitySet)
|
||||
entityType, err := service.LookupEntityType(entitySet.EntityType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = SemanticizeFilterQuery(req.Query.Filter, service, entityType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = SemanticizeExpandQuery(req.Query.Expand, service, entityType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = SemanticizeSelectQuery(req.Query.Select, service, entityType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = SemanticizeOrderByQuery(req.Query.OrderBy, service, entityType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO: disallow invalid query params
|
||||
case *GoDataEntityType:
|
||||
entityType := req.LastSegment.SemanticReference.(*GoDataEntityType)
|
||||
if err := SemanticizeExpandQuery(req.Query.Expand, service, entityType); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := SemanticizeSelectQuery(req.Query.Select, service, entityType); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if req.LastSegment.SemanticType == SemanticTypeMetadata {
|
||||
req.RequestKind = RequestKindMetadata
|
||||
} else if req.LastSegment.SemanticType == SemanticTypeRef {
|
||||
req.RequestKind = RequestKindRef
|
||||
} else if req.LastSegment.SemanticType == SemanticTypeEntitySet {
|
||||
if req.LastSegment.Identifier == nil {
|
||||
req.RequestKind = RequestKindCollection
|
||||
} else {
|
||||
req.RequestKind = RequestKindEntity
|
||||
}
|
||||
} else if req.LastSegment.SemanticType == SemanticTypeCount {
|
||||
req.RequestKind = RequestKindCount
|
||||
} else if req.FirstSegment == nil && req.LastSegment == nil {
|
||||
req.RequestKind = RequestKindService
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (req *GoDataRequest) ParseUrlPath(path string) error {
|
||||
parts := strings.Split(path, "/")
|
||||
req.FirstSegment = &GoDataSegment{
|
||||
RawValue: parts[0],
|
||||
Name: ParseName(parts[0]),
|
||||
Identifier: ParseIdentifiers(parts[0]),
|
||||
}
|
||||
currSegment := req.FirstSegment
|
||||
for _, v := range parts[1:] {
|
||||
temp := &GoDataSegment{
|
||||
RawValue: v,
|
||||
Name: ParseName(v),
|
||||
Identifier: ParseIdentifiers(v),
|
||||
Prev: currSegment,
|
||||
}
|
||||
currSegment.Next = temp
|
||||
currSegment = temp
|
||||
}
|
||||
req.LastSegment = currSegment
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SemanticizePathSegment(segment *GoDataSegment, service *GoDataService) error {
|
||||
var err error
|
||||
|
||||
if segment.RawValue == "$metadata" {
|
||||
if segment.Next != nil || segment.Prev != nil {
|
||||
return BadRequestError("A metadata segment must be alone.")
|
||||
}
|
||||
|
||||
segment.SemanticType = SemanticTypeMetadata
|
||||
segment.SemanticReference = service.Metadata
|
||||
return nil
|
||||
}
|
||||
|
||||
if segment.RawValue == "$ref" {
|
||||
// this is a ref segment
|
||||
if segment.Next != nil {
|
||||
return BadRequestError("A $ref segment must be last.")
|
||||
}
|
||||
if segment.Prev == nil {
|
||||
return BadRequestError("A $ref segment must be preceded by something.")
|
||||
}
|
||||
|
||||
segment.SemanticType = SemanticTypeRef
|
||||
segment.SemanticReference = segment.Prev
|
||||
return nil
|
||||
}
|
||||
|
||||
if segment.RawValue == "$count" {
|
||||
// this is a ref segment
|
||||
if segment.Next != nil {
|
||||
return BadRequestError("A $count segment must be last.")
|
||||
}
|
||||
if segment.Prev == nil {
|
||||
return BadRequestError("A $count segment must be preceded by something.")
|
||||
}
|
||||
|
||||
segment.SemanticType = SemanticTypeCount
|
||||
segment.SemanticReference = segment.Prev
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, ok := service.EntitySetLookup[segment.Name]; ok {
|
||||
// this is an entity set
|
||||
segment.SemanticType = SemanticTypeEntitySet
|
||||
segment.SemanticReference, err = service.LookupEntitySet(segment.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if segment.Prev == nil {
|
||||
// this is the first segment
|
||||
if segment.Next == nil {
|
||||
// this is the only segment
|
||||
return nil
|
||||
} else {
|
||||
// there is at least one more segment
|
||||
if segment.Identifier != nil {
|
||||
return BadRequestError("An entity set must be the last segment.")
|
||||
}
|
||||
// if it has an identifier, it is allowed
|
||||
return nil
|
||||
}
|
||||
} else if segment.Next == nil {
|
||||
// this is the last segment in a sequence of more than one
|
||||
return nil
|
||||
} else {
|
||||
// this is a middle segment
|
||||
if segment.Identifier != nil {
|
||||
return BadRequestError("An entity set must be the last segment.")
|
||||
}
|
||||
// if it has an identifier, it is allowed
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if segment.Prev != nil && segment.Prev.SemanticType == SemanticTypeEntitySet {
|
||||
// previous segment was an entity set
|
||||
semanticRef := segment.Prev.SemanticReference.(*GoDataEntitySet)
|
||||
|
||||
entity, err := service.LookupEntityType(semanticRef.EntityType)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, p := range entity.Properties {
|
||||
if p.Name == segment.Name {
|
||||
segment.SemanticType = SemanticTypeProperty
|
||||
segment.SemanticReference = p
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return BadRequestError("A valid entity property must follow entity set.")
|
||||
}
|
||||
|
||||
return BadRequestError("Invalid segment " + segment.RawValue)
|
||||
}
|
||||
|
||||
var supportedOdataKeywords = map[string]bool{
|
||||
"$filter": true,
|
||||
"$apply": true,
|
||||
"$expand": true,
|
||||
"$select": true,
|
||||
"$orderby": true,
|
||||
"$top": true,
|
||||
"$skip": true,
|
||||
"$count": true,
|
||||
"$inlinecount": true,
|
||||
"$search": true,
|
||||
"$compute": true,
|
||||
"$format": true,
|
||||
"at": true,
|
||||
"tags": true,
|
||||
}
|
||||
|
||||
type OdataComplianceConfig int
|
||||
|
||||
const (
|
||||
ComplianceStrict OdataComplianceConfig = 0
|
||||
// Ignore duplicate ODATA keywords in the URL query.
|
||||
ComplianceIgnoreDuplicateKeywords OdataComplianceConfig = 1 << iota
|
||||
// Ignore unknown ODATA keywords in the URL query.
|
||||
ComplianceIgnoreUnknownKeywords
|
||||
// Ignore extraneous comma as the last character in a list of function arguments.
|
||||
ComplianceIgnoreInvalidComma
|
||||
ComplianceIgnoreAll OdataComplianceConfig = ComplianceIgnoreDuplicateKeywords |
|
||||
ComplianceIgnoreUnknownKeywords |
|
||||
ComplianceIgnoreInvalidComma
|
||||
)
|
||||
|
||||
type parserConfigKey int
|
||||
|
||||
const (
|
||||
odataCompliance parserConfigKey = iota
|
||||
)
|
||||
|
||||
// If the lenient mode is set, the 'failOnConfig' bits are used to determine the ODATA compliance.
|
||||
// This is mostly for historical reasons because the original parser had compliance issues.
|
||||
// If the lenient mode is not set, the parser returns an error.
|
||||
func WithOdataComplianceConfig(ctx context.Context, cfg OdataComplianceConfig) context.Context {
|
||||
return context.WithValue(ctx, odataCompliance, cfg)
|
||||
}
|
||||
|
||||
// ParseUrlQuery parses the URL query, applying optional logic specified in the context.
|
||||
func (req *GoDataRequest) ParseUrlQuery(ctx context.Context, query url.Values) error {
|
||||
cfg, hasComplianceConfig := ctx.Value(odataCompliance).(OdataComplianceConfig)
|
||||
if !hasComplianceConfig {
|
||||
// Strict ODATA compliance by default.
|
||||
cfg = ComplianceStrict
|
||||
}
|
||||
// Validate each query parameter is a valid ODATA keyword.
|
||||
for key, val := range query {
|
||||
if _, ok := supportedOdataKeywords[key]; !ok && (cfg&ComplianceIgnoreUnknownKeywords == 0) {
|
||||
return BadRequestError(fmt.Sprintf("Query parameter '%s' is not supported", key)).
|
||||
SetCause(&UnsupportedQueryParameterError{key})
|
||||
}
|
||||
if (cfg&ComplianceIgnoreDuplicateKeywords == 0) && (len(val) > 1) {
|
||||
return BadRequestError(fmt.Sprintf("Query parameter '%s' cannot be specified more than once", key)).
|
||||
SetCause(&DuplicateQueryParameterError{key})
|
||||
}
|
||||
}
|
||||
filter := query.Get("$filter")
|
||||
at := query.Get("at")
|
||||
apply := query.Get("$apply")
|
||||
expand := query.Get("$expand")
|
||||
sel := query.Get("$select")
|
||||
orderby := query.Get("$orderby")
|
||||
top := query.Get("$top")
|
||||
skip := query.Get("$skip")
|
||||
count := query.Get("$count")
|
||||
inlinecount := query.Get("$inlinecount")
|
||||
search := query.Get("$search")
|
||||
compute := query.Get("$compute")
|
||||
format := query.Get("$format")
|
||||
|
||||
result := &GoDataQuery{}
|
||||
|
||||
var err error = nil
|
||||
if filter != "" {
|
||||
result.Filter, err = ParseFilterString(ctx, filter)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if at != "" {
|
||||
result.At, err = ParseFilterString(ctx, at)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if at != "" {
|
||||
result.At, err = ParseFilterString(ctx, at)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if apply != "" {
|
||||
result.Apply, err = ParseApplyString(ctx, apply)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if expand != "" {
|
||||
result.Expand, err = ParseExpandString(ctx, expand)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sel != "" {
|
||||
result.Select, err = ParseSelectString(ctx, sel)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if orderby != "" {
|
||||
result.OrderBy, err = ParseOrderByString(ctx, orderby)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if top != "" {
|
||||
result.Top, err = ParseTopString(ctx, top)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if skip != "" {
|
||||
result.Skip, err = ParseSkipString(ctx, skip)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count != "" {
|
||||
result.Count, err = ParseCountString(ctx, count)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if inlinecount != "" {
|
||||
result.InlineCount, err = ParseInlineCountString(ctx, inlinecount)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if search != "" {
|
||||
result.Search, err = ParseSearchString(ctx, search)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if compute != "" {
|
||||
result.Compute, err = ParseComputeString(ctx, compute)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if format != "" {
|
||||
err = NotImplementedError("Format is not supported")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Query = result
|
||||
return err
|
||||
}
|
||||
|
||||
func ParseIdentifiers(segment string) *GoDataIdentifier {
|
||||
if !(strings.Contains(segment, "(") && strings.Contains(segment, ")")) {
|
||||
return nil
|
||||
}
|
||||
|
||||
rawIds := segment[strings.LastIndex(segment, "(")+1 : strings.LastIndex(segment, ")")]
|
||||
parts := strings.Split(rawIds, ",")
|
||||
|
||||
result := make(GoDataIdentifier)
|
||||
|
||||
for _, v := range parts {
|
||||
if strings.Contains(v, "=") {
|
||||
split := strings.SplitN(v, "=", 2)
|
||||
result[split[0]] = split[1]
|
||||
} else {
|
||||
result[v] = ""
|
||||
}
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
func ParseName(segment string) string {
|
||||
if strings.Contains(segment, "(") {
|
||||
return segment[:strings.LastIndex(segment, "(")]
|
||||
} else {
|
||||
return segment
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user