mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-04 12:10:21 -05:00
410 lines
10 KiB
Go
410 lines
10 KiB
Go
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
|
|
}
|
|
}
|