safrano 0.4.3 → 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 +6 -2
- data/lib/odata/batch.rb +9 -7
- data/lib/odata/collection.rb +136 -642
- data/lib/odata/collection_filter.rb +16 -40
- data/lib/odata/collection_media.rb +56 -37
- data/lib/odata/collection_order.rb +5 -2
- data/lib/odata/common_logger.rb +2 -0
- data/lib/odata/complex_type.rb +152 -0
- data/lib/odata/edm/primitive_types.rb +184 -0
- data/lib/odata/entity.rb +53 -117
- data/lib/odata/error.rb +142 -37
- data/lib/odata/expand.rb +20 -17
- data/lib/odata/filter/base.rb +4 -1
- data/lib/odata/filter/error.rb +43 -27
- data/lib/odata/filter/parse.rb +33 -25
- data/lib/odata/filter/sequel.rb +97 -56
- data/lib/odata/filter/sequel_function_adapter.rb +50 -49
- data/lib/odata/filter/token.rb +10 -10
- data/lib/odata/filter/tree.rb +75 -41
- data/lib/odata/function_import.rb +166 -0
- data/lib/odata/model_ext.rb +618 -0
- data/lib/odata/navigation_attribute.rb +9 -24
- data/lib/odata/relations.rb +5 -5
- data/lib/odata/select.rb +17 -5
- data/lib/odata/transition.rb +71 -0
- data/lib/odata/url_parameters.rb +100 -24
- data/lib/odata/walker.rb +15 -7
- data/lib/safrano.rb +18 -38
- data/lib/safrano/contract.rb +143 -0
- data/lib/safrano/core.rb +12 -94
- data/lib/safrano/core_ext.rb +13 -0
- data/lib/safrano/deprecation.rb +73 -0
- data/lib/safrano/multipart.rb +25 -20
- data/lib/safrano/rack_app.rb +61 -62
- data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -1
- data/lib/safrano/request.rb +95 -37
- data/lib/safrano/response.rb +4 -2
- data/lib/safrano/sequel_join_by_paths.rb +2 -2
- data/lib/safrano/service.rb +132 -94
- data/lib/safrano/version.rb +3 -1
- data/lib/sequel/plugins/join_by_paths.rb +6 -19
- metadata +24 -5
data/lib/safrano/rack_app.rb
CHANGED
@@ -1,72 +1,47 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'rack'
|
4
|
-
require_relative '../odata/walker
|
5
|
-
require_relative 'request
|
6
|
-
require_relative 'response
|
4
|
+
require_relative '../odata/walker'
|
5
|
+
require_relative 'request'
|
6
|
+
require_relative 'response'
|
7
7
|
|
8
|
-
module
|
8
|
+
module Safrano
|
9
9
|
# handle GET PUT etc
|
10
10
|
module MethodHandlers
|
11
11
|
def odata_options
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
21
|
-
@response.headers['Content-Type'] = ''
|
22
|
-
x
|
23
|
-
end
|
24
|
-
|
25
|
-
def odata_error
|
26
|
-
return @walker.error.odata_get(@request) unless @walker.error.nil?
|
27
|
-
|
28
|
-
# this is too critical; raise a real Exception
|
29
|
-
raise 'Walker construction failed with a unknown Error '
|
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
|
30
20
|
end
|
31
21
|
|
32
22
|
def odata_delete
|
33
|
-
|
34
|
-
|
35
|
-
else
|
36
|
-
odata_error
|
37
|
-
end
|
23
|
+
@walker.finalize.tap_error { |err| return err.odata_get(@request) }
|
24
|
+
.if_valid { |context| context.odata_delete(@request) }
|
38
25
|
end
|
39
26
|
|
40
27
|
def odata_put
|
41
|
-
|
42
|
-
|
43
|
-
else
|
44
|
-
odata_error
|
45
|
-
end
|
28
|
+
@walker.finalize.tap_error { |err| return err.odata_get(@request) }
|
29
|
+
.if_valid { |context| context.odata_put(@request) }
|
46
30
|
end
|
47
31
|
|
48
32
|
def odata_patch
|
49
|
-
|
50
|
-
|
51
|
-
else
|
52
|
-
odata_error
|
53
|
-
end
|
33
|
+
@walker.finalize.tap_error { |err| return err.odata_get(@request) }
|
34
|
+
.if_valid { |context| context.odata_patch(@request) }
|
54
35
|
end
|
55
36
|
|
56
37
|
def odata_get
|
57
|
-
|
58
|
-
|
59
|
-
else
|
60
|
-
odata_error
|
61
|
-
end
|
38
|
+
@walker.finalize.tap_error { |err| return err.odata_get(@request) }
|
39
|
+
.if_valid { |context| context.odata_get(@request) }
|
62
40
|
end
|
63
41
|
|
64
42
|
def odata_post
|
65
|
-
|
66
|
-
|
67
|
-
else
|
68
|
-
odata_error
|
69
|
-
end
|
43
|
+
@walker.finalize.tap_error { |err| return err.odata_get(@request) }
|
44
|
+
.if_valid { |context| context.odata_post(@request) }
|
70
45
|
end
|
71
46
|
|
72
47
|
def odata_head
|
@@ -83,18 +58,15 @@ module OData
|
|
83
58
|
'Pragma' => 'no-cache' }.freeze
|
84
59
|
DATASERVICEVERSION = 'DataServiceVersion'.freeze
|
85
60
|
include MethodHandlers
|
61
|
+
|
86
62
|
def before
|
87
63
|
@request.service_base = self.class.get_service_base
|
88
64
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
myhdrs = NOCACHE_HDRS.dup
|
96
|
-
myhdrs[DATASERVICEVERSION] = @request.service.data_service_version
|
97
|
-
headers myhdrs
|
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
|
98
70
|
end
|
99
71
|
|
100
72
|
# dispatch for all methods requiring parsing of the path
|
@@ -119,6 +91,11 @@ module OData
|
|
119
91
|
end
|
120
92
|
end
|
121
93
|
|
94
|
+
def dispatch_error(err)
|
95
|
+
@response.status, rsph, @response.body = err.odata_get(@request)
|
96
|
+
headers rsph
|
97
|
+
end
|
98
|
+
|
122
99
|
def dispatch
|
123
100
|
req_ret = if @request.request_method !~ METHODS_REGEXP
|
124
101
|
[404, EMPTY_HASH, ['Did you get lost?']]
|
@@ -132,13 +109,23 @@ module OData
|
|
132
109
|
end
|
133
110
|
|
134
111
|
def call(env)
|
135
|
-
|
136
|
-
|
112
|
+
# for thread safety
|
113
|
+
dup._call(env)
|
114
|
+
end
|
137
115
|
|
138
|
-
|
116
|
+
def _call(env)
|
117
|
+
begin
|
118
|
+
@request = Safrano::Request.new(env)
|
119
|
+
@response = Safrano::Response.new
|
139
120
|
|
140
|
-
|
121
|
+
before.tap_error { |err| dispatch_error(err) }
|
122
|
+
.tap_valid { |res| dispatch }
|
141
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
|
142
129
|
@response.finish
|
143
130
|
end
|
144
131
|
|
@@ -167,10 +154,22 @@ module OData
|
|
167
154
|
end
|
168
155
|
|
169
156
|
def self.publish_service(&block)
|
170
|
-
sbase =
|
157
|
+
sbase = Safrano::ServiceBase.new
|
171
158
|
sbase.instance_eval(&block) if block_given?
|
172
159
|
sbase.finalize_publishing
|
173
160
|
set_servicebase(sbase)
|
174
161
|
end
|
175
162
|
end
|
176
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
|
@@ -15,3 +17,18 @@ module Rack
|
|
15
17
|
end
|
16
18
|
end
|
17
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
|
data/lib/safrano/request.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rack'
|
2
4
|
require 'rfc2047'
|
3
5
|
|
4
|
-
module
|
6
|
+
module Safrano
|
5
7
|
# monkey patch deactivate Rack/multipart because it does not work on simple
|
6
8
|
# OData $batch requests when the content-length
|
7
9
|
# is not passed
|
@@ -15,6 +17,7 @@ module OData
|
|
15
17
|
class AcceptEntry
|
16
18
|
attr_accessor :params
|
17
19
|
attr_reader :entry
|
20
|
+
|
18
21
|
def initialize(entry)
|
19
22
|
params = entry.scan(HEADER_PARAM).map! do |s|
|
20
23
|
key, value = s.strip.split('=', 2)
|
@@ -94,7 +97,9 @@ module OData
|
|
94
97
|
end
|
95
98
|
|
96
99
|
def create_odata_walker
|
97
|
-
@env['safrano.walker'] = @walker = Walker.new(@service,
|
100
|
+
@env['safrano.walker'] = @walker = Walker.new(@service,
|
101
|
+
path_info,
|
102
|
+
@content_id_references)
|
98
103
|
end
|
99
104
|
|
100
105
|
def accept
|
@@ -157,44 +162,97 @@ module OData
|
|
157
162
|
end
|
158
163
|
end
|
159
164
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
OData::MIN_DATASERVICE_VERSION
|
174
|
-
end
|
175
|
-
return OData::BadRequestError if minv.nil?
|
176
|
-
# client request an too new version --> 501
|
177
|
-
return OData::NotImplementedError if minv > OData::MAX_DATASERVICE_VERSION
|
178
|
-
return OData::BadRequestError if minv > maxv
|
179
|
-
|
180
|
-
v = if (rqv = env['HTTP_DATASERVICEVERSION'])
|
181
|
-
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
|
182
178
|
else
|
183
|
-
|
179
|
+
Contract.valid(maxv)
|
184
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
|
185
189
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
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
|
198
256
|
end
|
199
257
|
end
|
200
258
|
end
|
data/lib/safrano/response.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rack'
|
2
4
|
|
3
5
|
# monkey patch deactivate Rack/multipart because it does not work on simple
|
4
6
|
# OData $batch requests when the content-length is not passed
|
5
|
-
module
|
7
|
+
module Safrano
|
6
8
|
# borrowed fro Sinatra
|
7
9
|
# The response object. See Rack::Response and Rack::Response::Helpers for
|
8
10
|
# more info:
|
@@ -12,7 +14,7 @@ module OData
|
|
12
14
|
DROP_BODY_RESPONSES = [204, 205, 304].freeze
|
13
15
|
def initialize(*)
|
14
16
|
super
|
15
|
-
headers[
|
17
|
+
headers[CONTENT_TYPE] ||= APPJSON_UTF8
|
16
18
|
end
|
17
19
|
|
18
20
|
def body=(value)
|
data/lib/safrano/service.rb
CHANGED
@@ -1,14 +1,20 @@
|
|
1
|
-
|
2
|
-
require 'odata/relations.rb'
|
3
|
-
require 'odata/batch.rb'
|
4
|
-
require 'odata/error.rb'
|
5
|
-
require 'odata/filter/sequel.rb'
|
1
|
+
# frozen_string_literal: true
|
6
2
|
|
7
|
-
|
3
|
+
require 'rexml/document'
|
4
|
+
require 'odata/relations'
|
5
|
+
require 'odata/batch'
|
6
|
+
require 'odata/complex_type'
|
7
|
+
require 'odata/function_import'
|
8
|
+
require 'odata/error'
|
9
|
+
require 'odata/filter/sequel'
|
10
|
+
require 'set'
|
11
|
+
require 'odata/collection'
|
12
|
+
|
13
|
+
module Safrano
|
8
14
|
# this module has all methods related to expand/defered output preparation
|
9
15
|
# and will be included in Service class
|
10
16
|
module ExpandHandler
|
11
|
-
PATH_SPLITTER = %r{\A(\w+)
|
17
|
+
PATH_SPLITTER = %r{\A(\w+)/?(.*)\z}.freeze
|
12
18
|
DEFERRED = '__deferred'.freeze
|
13
19
|
URI = 'uri'.freeze
|
14
20
|
|
@@ -16,25 +22,6 @@ module OData
|
|
16
22
|
{ DEFERRED => { URI => "#{entity_uri}/#{attrib}" } }
|
17
23
|
end
|
18
24
|
|
19
|
-
# split expand argument into first and rest part
|
20
|
-
def split_entity_expand_arg(exp_one)
|
21
|
-
if exp_one.include?('/')
|
22
|
-
m = PATH_SPLITTER.match(exp_one)
|
23
|
-
cur_exp = m[1].strip
|
24
|
-
rest_exp = m[2]
|
25
|
-
# TODO: check errorhandling
|
26
|
-
raise OData::ServerError if cur_exp.nil?
|
27
|
-
|
28
|
-
k_s = cur_exp
|
29
|
-
|
30
|
-
else
|
31
|
-
k_s = exp_one.strip
|
32
|
-
rest_exp = nil
|
33
|
-
end
|
34
|
-
k = k_s.to_sym
|
35
|
-
yield k, k_s, rest_exp
|
36
|
-
end
|
37
|
-
|
38
25
|
# default v2
|
39
26
|
# overriden in ServiceV1
|
40
27
|
RESULTS_K = 'results'.freeze
|
@@ -43,11 +30,7 @@ module OData
|
|
43
30
|
array.map! do |w|
|
44
31
|
get_entity_odata_h(entity: w, template: template)
|
45
32
|
end
|
46
|
-
|
47
|
-
{ RESULTS_K => array, COUNT_K => icount }
|
48
|
-
else
|
49
|
-
{ RESULTS_K => array }
|
50
|
-
end
|
33
|
+
icount ? { RESULTS_K => array, COUNT_K => icount } : { RESULTS_K => array }
|
51
34
|
end
|
52
35
|
|
53
36
|
# handle $links ... Note: $expand seems to be ignored when $links
|
@@ -66,8 +49,10 @@ module OData
|
|
66
49
|
case elmt
|
67
50
|
when :all_values
|
68
51
|
hres.merge! entity.casted_values
|
52
|
+
|
69
53
|
when :selected_vals
|
70
54
|
hres.merge! entity.casted_values(arg)
|
55
|
+
|
71
56
|
when :expand_e
|
72
57
|
|
73
58
|
arg.each do |attr, templ|
|
@@ -79,6 +64,7 @@ module OData
|
|
79
64
|
EMPTYH
|
80
65
|
end
|
81
66
|
end
|
67
|
+
|
82
68
|
when :expand_c
|
83
69
|
arg.each do |attr, templ|
|
84
70
|
next unless (encoll = entity.send(attr))
|
@@ -87,6 +73,7 @@ module OData
|
|
87
73
|
hres[attr] = get_coll_odata_h(array: encoll, template: templ)
|
88
74
|
# else error ?
|
89
75
|
end
|
76
|
+
|
90
77
|
when :deferr
|
91
78
|
euri = entity.uri
|
92
79
|
arg.each do |attr|
|
@@ -99,7 +86,7 @@ module OData
|
|
99
86
|
end
|
100
87
|
end
|
101
88
|
|
102
|
-
module
|
89
|
+
module Safrano
|
103
90
|
# xml namespace constants needed for the output of XML service and
|
104
91
|
# and metadata
|
105
92
|
module XMLNS
|
@@ -115,17 +102,18 @@ module OData
|
|
115
102
|
end
|
116
103
|
|
117
104
|
# Link to Model
|
118
|
-
module
|
105
|
+
module Safrano
|
119
106
|
MAX_DATASERVICE_VERSION = '2'.freeze
|
120
107
|
MIN_DATASERVICE_VERSION = '1'.freeze
|
108
|
+
CV_MAX_DATASERVICE_VERSION = Contract.valid(MAX_DATASERVICE_VERSION).freeze
|
109
|
+
CV_MIN_DATASERVICE_VERSION = Contract.valid(MIN_DATASERVICE_VERSION).freeze
|
121
110
|
include XMLNS
|
122
111
|
# Base class for service. Subclass will be for V1, V2 etc...
|
123
112
|
class ServiceBase
|
124
113
|
include Safrano
|
125
114
|
include ExpandHandler
|
126
115
|
|
127
|
-
XML_PREAMBLE =
|
128
|
-
"\r\n".freeze
|
116
|
+
XML_PREAMBLE = %Q(<?xml version="1.0" encoding="utf-8" standalone="yes"?>\r\n).freeze
|
129
117
|
|
130
118
|
# This is just a hash of entity Set Names to the corresponding Class
|
131
119
|
# Example
|
@@ -148,6 +136,9 @@ module OData
|
|
148
136
|
attr_accessor :meta
|
149
137
|
attr_accessor :batch_handler
|
150
138
|
attr_accessor :relman
|
139
|
+
attr_accessor :complex_types
|
140
|
+
attr_accessor :function_imports
|
141
|
+
|
151
142
|
# Instance attributes for specialized Version specific Instances
|
152
143
|
attr_accessor :v1
|
153
144
|
attr_accessor :v2
|
@@ -161,37 +152,31 @@ module OData
|
|
161
152
|
# because of the version subclasses that dont use "super" initialise
|
162
153
|
# (todo: why not??)
|
163
154
|
@meta = ServiceMeta.new(self)
|
164
|
-
@batch_handler =
|
165
|
-
@relman =
|
155
|
+
@batch_handler = Safrano::Batch::DisabledHandler.new
|
156
|
+
@relman = Safrano::RelationManager.new
|
157
|
+
@complex_types = Set.new
|
158
|
+
@function_imports = {}
|
166
159
|
@cmap = {}
|
167
160
|
instance_eval(&block) if block_given?
|
168
161
|
end
|
169
162
|
|
170
|
-
DATASERVICEVERSION_RGX = /\A([1234])(?:\.0);*\w*\z/.freeze
|
171
163
|
TRAILING_SLASH = %r{/\z}.freeze
|
172
164
|
DEFAULT_PATH_PREFIX = '/'.freeze
|
173
|
-
DEFAULT_SERVER_URL = 'http://localhost:9494'
|
174
|
-
|
175
|
-
# input is the DataServiceVersion request header string, eg.
|
176
|
-
# '2.0;blabla' ---> Version -> 2
|
177
|
-
def self.parse_data_service_version(inp)
|
178
|
-
m = DATASERVICEVERSION_RGX.match(inp)
|
179
|
-
m[1] if m
|
180
|
-
end
|
165
|
+
DEFAULT_SERVER_URL = 'http://localhost:9494'.freeze
|
181
166
|
|
182
167
|
def enable_batch
|
183
|
-
@batch_handler =
|
168
|
+
@batch_handler = Safrano::Batch::EnabledHandler.new
|
184
169
|
(@v1.batch_handler = @batch_handler) if @v1
|
185
170
|
(@v2.batch_handler = @batch_handler) if @v2
|
186
171
|
end
|
187
172
|
|
188
173
|
def enable_v1_service
|
189
|
-
@v1 =
|
174
|
+
@v1 = Safrano::ServiceV1.new
|
190
175
|
copy_attribs_to @v1
|
191
176
|
end
|
192
177
|
|
193
178
|
def enable_v2_service
|
194
|
-
@v2 =
|
179
|
+
@v2 = Safrano::ServiceV2.new
|
195
180
|
copy_attribs_to @v2
|
196
181
|
end
|
197
182
|
|
@@ -225,12 +210,10 @@ module OData
|
|
225
210
|
def set_uribase
|
226
211
|
@uribase = if @xpath_prefix.empty?
|
227
212
|
@xserver_url
|
213
|
+
elsif @xpath_prefix[0] == '/'
|
214
|
+
"#{@xserver_url}#{@xpath_prefix}"
|
228
215
|
else
|
229
|
-
|
230
|
-
@xserver_url + @xpath_prefix
|
231
|
-
else
|
232
|
-
@xserver_url + '/' + @xpath_prefix
|
233
|
-
end
|
216
|
+
"#{@xserver_url}/#{@xpath_prefix}"
|
234
217
|
end
|
235
218
|
(@v1.uribase = @uribase) if @v1
|
236
219
|
(@v2.uribase = @uribase) if @v2
|
@@ -249,6 +232,8 @@ module OData
|
|
249
232
|
other.meta = ServiceMeta.new(other) # hum ... #todo: versions as well ?
|
250
233
|
other.relman = @relman
|
251
234
|
other.batch_handler = @batch_handler
|
235
|
+
other.complex_types = @complex_types
|
236
|
+
other.function_imports = @function_imports
|
252
237
|
other
|
253
238
|
end
|
254
239
|
|
@@ -261,38 +246,43 @@ module OData
|
|
261
246
|
def register_model(modelklass, entity_set_name = nil, is_media = false)
|
262
247
|
# check that the provided klass is a Sequel Model
|
263
248
|
|
264
|
-
raise(
|
249
|
+
raise(Safrano::API::ModelNameError, modelklass) unless modelklass.is_a? Sequel::Model::ClassMethods
|
265
250
|
|
266
|
-
if modelklass.ancestors.include?
|
251
|
+
if modelklass.ancestors.include? Safrano::Entity
|
267
252
|
# modules were already added previously;
|
268
253
|
# cleanup state to avoid having data from previous calls
|
269
254
|
# mostly usefull for testing (eg API)
|
270
255
|
modelklass.reset
|
271
256
|
elsif modelklass.primary_key.is_a?(Array) # first API call... (normal non-testing case)
|
272
|
-
modelklass.extend
|
273
|
-
modelklass.include
|
257
|
+
modelklass.extend Safrano::EntityClassMultiPK
|
258
|
+
modelklass.include Safrano::EntityMultiPK
|
274
259
|
else
|
275
|
-
modelklass.extend
|
276
|
-
modelklass.include
|
260
|
+
modelklass.extend Safrano::EntityClassSinglePK
|
261
|
+
modelklass.include Safrano::EntitySinglePK
|
277
262
|
end
|
263
|
+
|
278
264
|
# Media/Non-media
|
279
265
|
if is_media
|
280
|
-
modelklass.extend
|
266
|
+
modelklass.extend Safrano::EntityClassMedia
|
281
267
|
# set default media handler . Can be overridden later with the
|
282
268
|
# "use HandlerKlass, options" API
|
283
269
|
|
284
270
|
modelklass.set_default_media_handler
|
285
271
|
modelklass.api_check_media_fields
|
286
|
-
modelklass.include
|
272
|
+
modelklass.include Safrano::MediaEntity
|
287
273
|
else
|
288
|
-
modelklass.extend
|
289
|
-
modelklass.include
|
274
|
+
modelklass.extend Safrano::EntityClassNonMedia
|
275
|
+
modelklass.include Safrano::NonMediaEntity
|
290
276
|
end
|
291
277
|
|
292
278
|
modelklass.prepare_pk
|
293
279
|
modelklass.prepare_fields
|
294
280
|
esname = (entity_set_name || modelklass).to_s.freeze
|
295
|
-
|
281
|
+
serv_namespace = @xnamespace
|
282
|
+
modelklass.instance_eval do
|
283
|
+
@entity_set_name = esname
|
284
|
+
@namespace = serv_namespace
|
285
|
+
end
|
296
286
|
@cmap[esname] = modelklass
|
297
287
|
set_collections_sorted(@cmap.values)
|
298
288
|
end
|
@@ -313,6 +303,23 @@ module OData
|
|
313
303
|
modelklass.deferred_iblock = block if block_given?
|
314
304
|
end
|
315
305
|
|
306
|
+
def publish_complex_type(ctklass)
|
307
|
+
# check that the provided klass is a Safrano ComplexType
|
308
|
+
|
309
|
+
raise(Safrano::API::ComplexTypeNameError, ctklass) unless ctklass.superclass == Safrano::ComplexType
|
310
|
+
|
311
|
+
serv_namespace = @xnamespace
|
312
|
+
ctklass.instance_eval { @namespace = serv_namespace }
|
313
|
+
|
314
|
+
@complex_types.add ctklass
|
315
|
+
end
|
316
|
+
|
317
|
+
def function_import(name)
|
318
|
+
funcimp = Safrano::FunctionImport(name)
|
319
|
+
@function_imports[name] = funcimp
|
320
|
+
funcimp
|
321
|
+
end
|
322
|
+
|
316
323
|
def cmap=(imap)
|
317
324
|
@cmap = imap
|
318
325
|
set_collections_sorted(@cmap.values)
|
@@ -352,14 +359,10 @@ module OData
|
|
352
359
|
@collections.each(&:finalize_publishing)
|
353
360
|
|
354
361
|
# finalize the uri's and include NoMappingBeforeOutput or MappingBeforeOutput as needed
|
355
|
-
@collections.each
|
362
|
+
@collections.each do |klass|
|
356
363
|
klass.build_uri(@uribase)
|
357
|
-
|
358
|
-
|
359
|
-
else
|
360
|
-
klass.include OData::MappingBeforeOutput
|
361
|
-
end
|
362
|
-
}
|
364
|
+
klass.include(klass.time_cols.empty? ? Safrano::NoMappingBeforeOutput : Safrano::MappingBeforeOutput)
|
365
|
+
end
|
363
366
|
|
364
367
|
# build allowed transitions (requires that @collections are filled and sorted for having a
|
365
368
|
# correct base_url_regexp)
|
@@ -368,11 +371,11 @@ module OData
|
|
368
371
|
# mixin adapter specific modules where needed
|
369
372
|
case Sequel::Model.db.adapter_scheme
|
370
373
|
when :postgres
|
371
|
-
|
374
|
+
Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreePostgres
|
372
375
|
when :sqlite
|
373
|
-
|
376
|
+
Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreeSqlite
|
374
377
|
else
|
375
|
-
|
378
|
+
Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreeDefault
|
376
379
|
end
|
377
380
|
end
|
378
381
|
|
@@ -389,19 +392,24 @@ module OData
|
|
389
392
|
@collections.map(&:entity_set_name).join('|')
|
390
393
|
end
|
391
394
|
|
395
|
+
def base_url_func_regexp
|
396
|
+
@function_imports.keys.join('|')
|
397
|
+
end
|
398
|
+
|
392
399
|
def service
|
393
400
|
hres = {}
|
394
401
|
hres['d'] = { 'EntitySets' => @collections.map(&:type_name) }
|
395
402
|
hres
|
396
403
|
end
|
397
404
|
|
398
|
-
def service_xml(
|
405
|
+
def service_xml(_req)
|
399
406
|
doc = REXML::Document.new
|
400
407
|
# separator required ? ?
|
401
408
|
root = doc.add_element('service', 'xml:base' => @uribase)
|
402
409
|
|
403
410
|
root.add_namespace('xmlns:atom', XMLNS::W3_2005_ATOM)
|
404
411
|
root.add_namespace('xmlns:app', XMLNS::W3_2007_APP)
|
412
|
+
|
405
413
|
# this generates the main xmlns attribute
|
406
414
|
root.add_namespace(XMLNS::W3_2007_APP)
|
407
415
|
wp = root.add_element 'workspace'
|
@@ -412,7 +420,7 @@ module OData
|
|
412
420
|
@collections.each do |klass|
|
413
421
|
col = wp.add_element('collection', 'href' => klass.entity_set_name)
|
414
422
|
ct = col.add_element('atom:title')
|
415
|
-
ct.text = klass.
|
423
|
+
ct.text = klass.entity_set_name
|
416
424
|
end
|
417
425
|
|
418
426
|
XML_PREAMBLE + doc.to_pretty_xml
|
@@ -421,10 +429,18 @@ module OData
|
|
421
429
|
def add_metadata_xml_entity_type(schema)
|
422
430
|
@collections.each do |klass|
|
423
431
|
enty = klass.add_metadata_rexml(schema)
|
424
|
-
klass.add_metadata_navs_rexml(enty, @relman
|
432
|
+
klass.add_metadata_navs_rexml(enty, @relman)
|
425
433
|
end
|
426
434
|
end
|
427
435
|
|
436
|
+
def add_metadata_xml_complex_types(schema)
|
437
|
+
@complex_types.each { |ctklass| ctklass.add_metadata_rexml(schema) }
|
438
|
+
end
|
439
|
+
|
440
|
+
def add_metadata_xml_function_imports(ec)
|
441
|
+
@function_imports.each_value { |func| func.add_metadata_rexml(ec) }
|
442
|
+
end
|
443
|
+
|
428
444
|
def add_metadata_xml_associations(schema)
|
429
445
|
@relman.each_rel do |rel|
|
430
446
|
rel.with_metadata_info(@xnamespace) do |name, bdinfo|
|
@@ -447,7 +463,7 @@ module OData
|
|
447
463
|
# 3.a Entity set's
|
448
464
|
ec.add_element('EntitySet',
|
449
465
|
'Name' => klass.entity_set_name,
|
450
|
-
'EntityType' =>
|
466
|
+
'EntityType' => klass.type_name)
|
451
467
|
end
|
452
468
|
# 3.b Association set's
|
453
469
|
@relman.each_rel do |rel|
|
@@ -461,6 +477,9 @@ module OData
|
|
461
477
|
assoc.add_element('End', assoend)
|
462
478
|
end
|
463
479
|
end
|
480
|
+
|
481
|
+
# 4 function imports
|
482
|
+
add_metadata_xml_function_imports(ec)
|
464
483
|
end
|
465
484
|
|
466
485
|
def metadata_xml(_req)
|
@@ -494,9 +513,12 @@ module OData
|
|
494
513
|
schema = serv.add_element('Schema',
|
495
514
|
'Namespace' => @xnamespace,
|
496
515
|
'xmlns' => XMLNS::MSFT_ADO_2009_EDM)
|
497
|
-
# 1. all EntityType
|
516
|
+
# 1. a. all EntityType
|
498
517
|
add_metadata_xml_entity_type(schema)
|
499
518
|
|
519
|
+
# 1. b. all ComplexType
|
520
|
+
add_metadata_xml_complex_types(schema)
|
521
|
+
|
500
522
|
# 2. Associations
|
501
523
|
add_metadata_xml_associations(schema)
|
502
524
|
|
@@ -517,18 +539,36 @@ module OData
|
|
517
539
|
].freeze
|
518
540
|
|
519
541
|
def build_allowed_transitions
|
520
|
-
@allowed_transitions =
|
521
|
-
|
522
|
-
|
523
|
-
|
542
|
+
@allowed_transitions = if @function_imports.empty?
|
543
|
+
(ALLOWED_TRANSITIONS_FIXED + [
|
544
|
+
Safrano::Transition.new(%r{\A/(#{base_url_regexp})(.*)},
|
545
|
+
trans: 'transition_collection')
|
546
|
+
]).freeze
|
547
|
+
else
|
548
|
+
(ALLOWED_TRANSITIONS_FIXED + [
|
549
|
+
Safrano::Transition.new(%r{\A/(#{base_url_regexp})(.*)},
|
550
|
+
trans: 'transition_collection'),
|
551
|
+
Safrano::Transition.new(%r{\A/(#{base_url_func_regexp})(.*)},
|
552
|
+
trans: 'transition_service_op')
|
553
|
+
]).freeze
|
554
|
+
end
|
524
555
|
end
|
525
556
|
|
526
557
|
def allowed_transitions
|
527
558
|
@allowed_transitions
|
528
559
|
end
|
529
560
|
|
561
|
+
def thread_safe_collection(collklass)
|
562
|
+
Safrano::OData::Collection.new(collklass)
|
563
|
+
end
|
564
|
+
|
530
565
|
def transition_collection(match_result)
|
531
|
-
[@cmap[match_result[1]], :run] if match_result[1]
|
566
|
+
[thread_safe_collection(@cmap[match_result[1]]), :run] if match_result[1]
|
567
|
+
# [@cmap[match_result[1]], :run] if match_result[1]
|
568
|
+
end
|
569
|
+
|
570
|
+
def transition_service_op(match_result)
|
571
|
+
[@function_imports[match_result[1]], :run] if match_result[1]
|
532
572
|
end
|
533
573
|
|
534
574
|
def transition_batch(_match_result)
|
@@ -544,7 +584,7 @@ module OData
|
|
544
584
|
end
|
545
585
|
|
546
586
|
def transition_end(_match_result)
|
547
|
-
|
587
|
+
Safrano::Transition::RESULT_END
|
548
588
|
end
|
549
589
|
end
|
550
590
|
|
@@ -552,14 +592,11 @@ module OData
|
|
552
592
|
|
553
593
|
def odata_get(req)
|
554
594
|
if req.accept?(APPXML)
|
555
|
-
#
|
556
|
-
#
|
557
|
-
|
558
|
-
# identified with the "application/atomsvc+xml" media type (see
|
559
|
-
# [RFC5023] section 8).
|
560
|
-
[200, CT_ATOMXML, [service_xml(req)]]
|
595
|
+
# OData V2 reference service implementations are returning app-xml-u8
|
596
|
+
# so we do
|
597
|
+
[200, CT_APPXML, [service_xml(req)]]
|
561
598
|
else
|
562
|
-
# this is returned by http://services.odata.org/V2/OData/
|
599
|
+
# this is returned by http://services.odata.org/V2/OData/Safrano.svc
|
563
600
|
415
|
564
601
|
end
|
565
602
|
end
|
@@ -615,6 +652,7 @@ module OData
|
|
615
652
|
# a virtual entity for the service metadata
|
616
653
|
class ServiceMeta
|
617
654
|
attr_accessor :service
|
655
|
+
|
618
656
|
def initialize(service)
|
619
657
|
@service = service
|
620
658
|
end
|
@@ -625,7 +663,7 @@ module OData
|
|
625
663
|
end
|
626
664
|
|
627
665
|
def transition_end(_match_result)
|
628
|
-
|
666
|
+
Safrano::Transition::RESULT_END
|
629
667
|
end
|
630
668
|
|
631
669
|
def odata_get(req)
|