safrano 0.3.4 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/core_ext/Dir/iter.rb +18 -0
- data/lib/core_ext/Hash/transform.rb +21 -0
- data/lib/core_ext/Integer/edm.rb +13 -0
- data/lib/core_ext/REXML/Document/output.rb +16 -0
- data/lib/core_ext/String/convert.rb +25 -0
- data/lib/core_ext/String/edm.rb +13 -0
- data/lib/core_ext/dir.rb +3 -0
- data/lib/core_ext/hash.rb +3 -0
- data/lib/core_ext/integer.rb +3 -0
- data/lib/core_ext/rexml.rb +3 -0
- data/lib/core_ext/string.rb +5 -0
- data/lib/odata/attribute.rb +15 -10
- data/lib/odata/batch.rb +17 -15
- data/lib/odata/collection.rb +141 -500
- data/lib/odata/collection_filter.rb +44 -37
- data/lib/odata/collection_media.rb +193 -43
- data/lib/odata/collection_order.rb +50 -37
- data/lib/odata/common_logger.rb +39 -12
- data/lib/odata/complex_type.rb +152 -0
- data/lib/odata/edm/primitive_types.rb +184 -0
- data/lib/odata/entity.rb +201 -176
- data/lib/odata/error.rb +186 -33
- data/lib/odata/expand.rb +126 -0
- data/lib/odata/filter/base.rb +69 -0
- data/lib/odata/filter/error.rb +55 -6
- data/lib/odata/filter/parse.rb +38 -36
- data/lib/odata/filter/sequel.rb +121 -67
- data/lib/odata/filter/sequel_function_adapter.rb +148 -0
- data/lib/odata/filter/token.rb +15 -11
- data/lib/odata/filter/tree.rb +110 -60
- data/lib/odata/function_import.rb +166 -0
- data/lib/odata/model_ext.rb +618 -0
- data/lib/odata/navigation_attribute.rb +50 -32
- data/lib/odata/relations.rb +7 -7
- data/lib/odata/select.rb +54 -0
- data/lib/{safrano_core.rb → odata/transition.rb} +14 -60
- data/lib/odata/url_parameters.rb +128 -37
- data/lib/odata/walker.rb +19 -11
- data/lib/safrano.rb +18 -28
- data/lib/safrano/contract.rb +143 -0
- data/lib/safrano/core.rb +43 -0
- data/lib/safrano/core_ext.rb +13 -0
- data/lib/safrano/deprecation.rb +73 -0
- data/lib/{multipart.rb → safrano/multipart.rb} +37 -41
- data/lib/safrano/rack_app.rb +175 -0
- data/lib/{odata_rack_builder.rb → safrano/rack_builder.rb} +18 -2
- data/lib/{request.rb → safrano/request.rb} +102 -50
- data/lib/{response.rb → safrano/response.rb} +5 -4
- data/lib/safrano/sequel_join_by_paths.rb +5 -0
- data/lib/{service.rb → safrano/service.rb} +257 -188
- data/lib/safrano/version.rb +5 -0
- data/lib/sequel/plugins/join_by_paths.rb +17 -29
- metadata +53 -17
- data/lib/rack_app.rb +0 -174
- data/lib/sequel_join_by_paths.rb +0 -5
- data/lib/version.rb +0 -4
@@ -0,0 +1,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack'
|
4
|
+
require_relative '../odata/walker'
|
5
|
+
require_relative 'request'
|
6
|
+
require_relative 'response'
|
7
|
+
|
8
|
+
module Safrano
|
9
|
+
# handle GET PUT etc
|
10
|
+
module MethodHandlers
|
11
|
+
def odata_options
|
12
|
+
@walker.finalize.tap_error { |err| return err.odata_get(@request) }
|
13
|
+
.if_valid do |context|
|
14
|
+
# cf. stackoverflow.com/questions/22924678/sinatra-delete-response-headers
|
15
|
+
headers.delete('Content-Type')
|
16
|
+
@response.headers.delete('Content-Type')
|
17
|
+
@response.headers['Content-Type'] = ''
|
18
|
+
[200, EMPTY_HASH, '']
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def odata_delete
|
23
|
+
@walker.finalize.tap_error { |err| return err.odata_get(@request) }
|
24
|
+
.if_valid { |context| context.odata_delete(@request) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def odata_put
|
28
|
+
@walker.finalize.tap_error { |err| return err.odata_get(@request) }
|
29
|
+
.if_valid { |context| context.odata_put(@request) }
|
30
|
+
end
|
31
|
+
|
32
|
+
def odata_patch
|
33
|
+
@walker.finalize.tap_error { |err| return err.odata_get(@request) }
|
34
|
+
.if_valid { |context| context.odata_patch(@request) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def odata_get
|
38
|
+
@walker.finalize.tap_error { |err| return err.odata_get(@request) }
|
39
|
+
.if_valid { |context| context.odata_get(@request) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def odata_post
|
43
|
+
@walker.finalize.tap_error { |err| return err.odata_get(@request) }
|
44
|
+
.if_valid { |context| context.odata_post(@request) }
|
45
|
+
end
|
46
|
+
|
47
|
+
def odata_head
|
48
|
+
[200, EMPTY_HASH, [EMPTY_STRING]]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# the main Rack server app. Source: the Rack docu/examples and partly
|
53
|
+
# inspired from Sinatra
|
54
|
+
class ServerApp
|
55
|
+
METHODS_REGEXP = Regexp.new('HEAD|OPTIONS|GET|POST|PATCH|MERGE|PUT|DELETE').freeze
|
56
|
+
NOCACHE_HDRS = { 'Cache-Control' => 'no-cache',
|
57
|
+
'Expires' => '-1',
|
58
|
+
'Pragma' => 'no-cache' }.freeze
|
59
|
+
DATASERVICEVERSION = 'DataServiceVersion'.freeze
|
60
|
+
include MethodHandlers
|
61
|
+
|
62
|
+
def before
|
63
|
+
@request.service_base = self.class.get_service_base
|
64
|
+
|
65
|
+
@request.negotiate_service_version.tap_valid do
|
66
|
+
myhdrs = NOCACHE_HDRS.dup
|
67
|
+
myhdrs[DATASERVICEVERSION] = @request.service.data_service_version
|
68
|
+
headers myhdrs
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# dispatch for all methods requiring parsing of the path
|
73
|
+
# with walker (ie. allmost all excepted HEAD)
|
74
|
+
def dispatch_with_walker
|
75
|
+
@walker = @request.create_odata_walker
|
76
|
+
case @request.request_method
|
77
|
+
when 'GET'
|
78
|
+
odata_get
|
79
|
+
when 'POST'
|
80
|
+
odata_post
|
81
|
+
when 'DELETE'
|
82
|
+
odata_delete
|
83
|
+
when 'OPTIONS'
|
84
|
+
odata_options
|
85
|
+
when 'PUT'
|
86
|
+
odata_put
|
87
|
+
when 'PATCH', 'MERGE'
|
88
|
+
odata_patch
|
89
|
+
else
|
90
|
+
raise Error
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def dispatch_error(err)
|
95
|
+
@response.status, rsph, @response.body = err.odata_get(@request)
|
96
|
+
headers rsph
|
97
|
+
end
|
98
|
+
|
99
|
+
def dispatch
|
100
|
+
req_ret = if @request.request_method !~ METHODS_REGEXP
|
101
|
+
[404, EMPTY_HASH, ['Did you get lost?']]
|
102
|
+
elsif @request.request_method == 'HEAD'
|
103
|
+
odata_head
|
104
|
+
else
|
105
|
+
dispatch_with_walker
|
106
|
+
end
|
107
|
+
@response.status, rsph, @response.body = req_ret
|
108
|
+
headers rsph
|
109
|
+
end
|
110
|
+
|
111
|
+
def call(env)
|
112
|
+
# for thread safety
|
113
|
+
dup._call(env)
|
114
|
+
end
|
115
|
+
|
116
|
+
def _call(env)
|
117
|
+
begin
|
118
|
+
@request = Safrano::Request.new(env)
|
119
|
+
@response = Safrano::Response.new
|
120
|
+
|
121
|
+
before.tap_error { |err| dispatch_error(err) }
|
122
|
+
.tap_valid { |res| dispatch }
|
123
|
+
|
124
|
+
# handle remaining Sequel errors that we couldnt prevent with our
|
125
|
+
# own pre-checks
|
126
|
+
rescue Sequel::Error => e
|
127
|
+
dispatch_error(SequelExceptionError.new(e))
|
128
|
+
end
|
129
|
+
@response.finish
|
130
|
+
end
|
131
|
+
|
132
|
+
# Set multiple response headers with Hash.
|
133
|
+
def headers(hash = nil)
|
134
|
+
@response.headers.merge! hash if hash
|
135
|
+
@response.headers
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.enable_batch
|
139
|
+
@service_base.enable_batch
|
140
|
+
end
|
141
|
+
|
142
|
+
def self.path_prefix(path_pr)
|
143
|
+
@service_base.path_prefix path_pr
|
144
|
+
end
|
145
|
+
|
146
|
+
def self.get_service_base
|
147
|
+
@service_base
|
148
|
+
end
|
149
|
+
|
150
|
+
def self.set_servicebase(sbase)
|
151
|
+
@service_base = sbase
|
152
|
+
@service_base.enable_v1_service
|
153
|
+
@service_base.enable_v2_service
|
154
|
+
end
|
155
|
+
|
156
|
+
def self.publish_service(&block)
|
157
|
+
sbase = Safrano::ServiceBase.new
|
158
|
+
sbase.instance_eval(&block) if block_given?
|
159
|
+
sbase.finalize_publishing
|
160
|
+
set_servicebase(sbase)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# deprecated
|
166
|
+
# REMOVE 0.6
|
167
|
+
module OData
|
168
|
+
class ServerApp < Safrano::ServerApp
|
169
|
+
def self.publish_service(&block)
|
170
|
+
::Safrano::Deprecation.deprecate('OData::ServerApp',
|
171
|
+
'Use Safrano::ServerApp instead')
|
172
|
+
super
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rack'
|
2
4
|
require 'rack/cors'
|
3
5
|
|
4
6
|
module Rack
|
5
|
-
module
|
7
|
+
module Safrano
|
6
8
|
# just a Wrapper to ensure (force?) that mandatory middlewares are acutally
|
7
9
|
# used
|
8
10
|
class Builder < ::Rack::Builder
|
@@ -10,9 +12,23 @@ module Rack
|
|
10
12
|
super(default_app) {}
|
11
13
|
use ::Rack::Cors
|
12
14
|
instance_eval(&block) if block_given?
|
13
|
-
use ::Rack::Lint
|
14
15
|
use ::Rack::ContentLength
|
15
16
|
end
|
16
17
|
end
|
17
18
|
end
|
18
19
|
end
|
20
|
+
|
21
|
+
# deprecated
|
22
|
+
# REMOVE 0.6
|
23
|
+
module Rack
|
24
|
+
module OData
|
25
|
+
class Builder < ::Rack::Safrano::Builder
|
26
|
+
def initialize(default_app = nil, &block)
|
27
|
+
::Safrano::Deprecation.deprecate('Rack::OData::Builder',
|
28
|
+
'Use Rack::Safrano::Builder instead')
|
29
|
+
|
30
|
+
super
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -1,8 +1,9 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'rack'
|
4
|
+
require 'rfc2047'
|
4
5
|
|
5
|
-
module
|
6
|
+
module Safrano
|
6
7
|
# monkey patch deactivate Rack/multipart because it does not work on simple
|
7
8
|
# OData $batch requests when the content-length
|
8
9
|
# is not passed
|
@@ -16,6 +17,7 @@ module OData
|
|
16
17
|
class AcceptEntry
|
17
18
|
attr_accessor :params
|
18
19
|
attr_reader :entry
|
20
|
+
|
19
21
|
def initialize(entry)
|
20
22
|
params = entry.scan(HEADER_PARAM).map! do |s|
|
21
23
|
key, value = s.strip.split('=', 2)
|
@@ -95,7 +97,9 @@ module OData
|
|
95
97
|
end
|
96
98
|
|
97
99
|
def create_odata_walker
|
98
|
-
@env['safrano.walker'] = @walker = Walker.new(@service,
|
100
|
+
@env['safrano.walker'] = @walker = Walker.new(@service,
|
101
|
+
path_info,
|
102
|
+
@content_id_references)
|
99
103
|
end
|
100
104
|
|
101
105
|
def accept
|
@@ -109,13 +113,6 @@ module OData
|
|
109
113
|
end
|
110
114
|
end
|
111
115
|
|
112
|
-
def uribase
|
113
|
-
return @uribase if @uribase
|
114
|
-
|
115
|
-
@uribase =
|
116
|
-
"#{env['rack.url_scheme']}://#{env['HTTP_HOST']}#{service.xpath_prefix}"
|
117
|
-
end
|
118
|
-
|
119
116
|
def accept?(type)
|
120
117
|
preferred_type(type).to_s.include?(type)
|
121
118
|
end
|
@@ -136,11 +133,13 @@ module OData
|
|
136
133
|
def with_media_data
|
137
134
|
if (filename = @env['HTTP_SLUG'])
|
138
135
|
|
139
|
-
yield @env['rack.input'],
|
136
|
+
yield @env['rack.input'],
|
137
|
+
content_type.split(';').first,
|
138
|
+
Rfc2047.decode(filename)
|
140
139
|
|
141
140
|
else
|
142
141
|
ON_CGST_ERROR.call(self)
|
143
|
-
|
142
|
+
[400, EMPTY_HASH, ['File upload error: Missing SLUG']]
|
144
143
|
end
|
145
144
|
end
|
146
145
|
|
@@ -151,56 +150,109 @@ module OData
|
|
151
150
|
data = JSON.parse(body.read)
|
152
151
|
rescue JSON::ParserError => e
|
153
152
|
ON_CGST_ERROR.call(self)
|
154
|
-
return [400,
|
155
|
-
|
153
|
+
return [400, EMPTY_HASH, ['JSON Parser Error while parsing payload : ',
|
154
|
+
e.message]]
|
156
155
|
end
|
157
156
|
|
158
157
|
yield data
|
159
158
|
|
160
159
|
else # TODO: other formats
|
161
160
|
|
162
|
-
[415,
|
161
|
+
[415, EMPTY_HASH, EMPTY_ARRAY]
|
163
162
|
end
|
164
163
|
end
|
165
164
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
OData::MIN_DATASERVICE_VERSION
|
180
|
-
end
|
181
|
-
return OData::BadRequestError if minv.nil?
|
182
|
-
# client request an too new version --> 501
|
183
|
-
return OData::NotImplementedError if minv > OData::MAX_DATASERVICE_VERSION
|
184
|
-
return OData::BadRequestError if minv > maxv
|
185
|
-
|
186
|
-
v = if (rqv = env['HTTP_DATASERVICEVERSION'])
|
187
|
-
OData::ServiceBase.parse_data_service_version(rqv)
|
165
|
+
# input is the DataServiceVersion request header string, eg.
|
166
|
+
# '2.0;blabla' ---> Version -> 2
|
167
|
+
DATASERVICEVERSION_RGX = /\A([1234])(?:\.0);*\w*\z/.freeze
|
168
|
+
|
169
|
+
MAX_DTSV_PARSE_ERROR = Safrano::BadRequestError.new(
|
170
|
+
'MaxDataServiceVersion could not be parsed'
|
171
|
+
).freeze
|
172
|
+
def get_maxversion
|
173
|
+
if (rqv = env['HTTP_MAXDATASERVICEVERSION'])
|
174
|
+
if (m = DATASERVICEVERSION_RGX.match(rqv))
|
175
|
+
# client request an too old version --> 501
|
176
|
+
if (maxv = m[1]) < Safrano::MIN_DATASERVICE_VERSION
|
177
|
+
Safrano::VersionNotImplementedError
|
188
178
|
else
|
189
|
-
|
179
|
+
Contract.valid(maxv)
|
190
180
|
end
|
181
|
+
else
|
182
|
+
MAX_DTSV_PARSE_ERROR
|
183
|
+
end
|
184
|
+
else
|
185
|
+
# not provided in request header --> take ours
|
186
|
+
Safrano::CV_MAX_DATASERVICE_VERSION
|
187
|
+
end
|
188
|
+
end
|
191
189
|
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
190
|
+
DTSV_PARSE_ERROR = Safrano::BadRequestError.new(
|
191
|
+
'DataServiceVersion could not be parsed'
|
192
|
+
).freeze
|
193
|
+
def get_version
|
194
|
+
if (rqv = env['HTTP_DATASERVICEVERSION'])
|
195
|
+
if (m = DATASERVICEVERSION_RGX.match(rqv))
|
196
|
+
# client request an too new version --> 501
|
197
|
+
if ((v = m[1]) > Safrano::MAX_DATASERVICE_VERSION) or
|
198
|
+
(v < Safrano::MIN_DATASERVICE_VERSION)
|
199
|
+
Safrano::VersionNotImplementedError
|
200
|
+
else
|
201
|
+
Contract.valid(v)
|
202
|
+
end
|
203
|
+
else
|
204
|
+
DTSV_PARSE_ERROR
|
205
|
+
end
|
206
|
+
else
|
207
|
+
# not provided in request header --> take our maxv
|
208
|
+
Safrano::CV_MAX_DATASERVICE_VERSION
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
MIN_DTSV_PARSE_ERROR = Safrano::BadRequestError.new(
|
213
|
+
'MinDataServiceVersion could not be parsed'
|
214
|
+
).freeze
|
215
|
+
def get_minversion
|
216
|
+
if (rqv = env['HTTP_MINDATASERVICEVERSION'])
|
217
|
+
if (m = DATASERVICEVERSION_RGX.match(rqv))
|
218
|
+
# client request an too new version --> 501
|
219
|
+
if (minv = m[1]) > Safrano::MAX_DATASERVICE_VERSION
|
220
|
+
Safrano::VersionNotImplementedError
|
221
|
+
else
|
222
|
+
Contract.valid(minv)
|
223
|
+
end
|
224
|
+
else
|
225
|
+
MIN_DTSV_PARSE_ERROR
|
226
|
+
end
|
227
|
+
else
|
228
|
+
# not provided in request header --> take ours
|
229
|
+
Safrano::CV_MIN_DATASERVICE_VERSION
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
MAX_LT_MIN_DTSV_ERROR = Safrano::BadRequestError.new(
|
234
|
+
'MinDataServiceVersion is larger as MaxDataServiceVersion'
|
235
|
+
).freeze
|
236
|
+
def negotiate_service_version
|
237
|
+
get_maxversion.if_valid do |maxv|
|
238
|
+
get_minversion.if_valid do |minv|
|
239
|
+
return MAX_LT_MIN_DTSV_ERROR if minv > maxv
|
240
|
+
|
241
|
+
get_version.if_valid do |v|
|
242
|
+
@service = nil
|
243
|
+
@service = case maxv
|
244
|
+
when '1'
|
245
|
+
@service_base.v1
|
246
|
+
when '2', '3', '4'
|
247
|
+
@service_base.v2
|
248
|
+
else
|
249
|
+
return Safrano::VersionNotImplementedError
|
250
|
+
end
|
251
|
+
|
252
|
+
Contract::OK
|
253
|
+
end # valid get_version
|
254
|
+
end # valid get_minversion
|
255
|
+
end # valid get_maxversion
|
204
256
|
end
|
205
257
|
end
|
206
258
|
end
|
@@ -1,9 +1,10 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'rack'
|
3
4
|
|
4
5
|
# monkey patch deactivate Rack/multipart because it does not work on simple
|
5
6
|
# OData $batch requests when the content-length is not passed
|
6
|
-
module
|
7
|
+
module Safrano
|
7
8
|
# borrowed fro Sinatra
|
8
9
|
# The response object. See Rack::Response and Rack::Response::Helpers for
|
9
10
|
# more info:
|
@@ -13,7 +14,7 @@ module OData
|
|
13
14
|
DROP_BODY_RESPONSES = [204, 205, 304].freeze
|
14
15
|
def initialize(*)
|
15
16
|
super
|
16
|
-
headers[
|
17
|
+
headers[CONTENT_TYPE] ||= APPJSON_UTF8
|
17
18
|
end
|
18
19
|
|
19
20
|
def body=(value)
|
@@ -35,7 +36,7 @@ module OData
|
|
35
36
|
|
36
37
|
if drop_body?
|
37
38
|
close
|
38
|
-
result =
|
39
|
+
result = EMPTY_ARRAY
|
39
40
|
end
|
40
41
|
|
41
42
|
if calculate_content_length?
|