Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+9
View File
@@ -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
View File
@@ -0,0 +1,58 @@
[![Go](https://github.com/CiscoM31/godata/actions/workflows/go.yml/badge.svg)](https://github.com/CiscoM31/godata/actions/workflows/go.yml)
[![golangci-lint](https://github.com/CiscoM31/godata/actions/workflows/golangci-lint.yml/badge.svg)](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
View File
@@ -0,0 +1,8 @@
package godata
import "context"
func ParseApplyString(ctx context.Context, apply string) (*GoDataApplyQuery, error) {
result := GoDataApplyQuery(apply)
return &result, nil
}
+149
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
}