safrano 0.4.0 → 0.4.5
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 +15 -13
- data/lib/odata/collection.rb +144 -535
- data/lib/odata/collection_filter.rb +47 -40
- data/lib/odata/collection_media.rb +145 -74
- data/lib/odata/collection_order.rb +50 -37
- data/lib/odata/common_logger.rb +36 -34
- data/lib/odata/complex_type.rb +152 -0
- data/lib/odata/edm/primitive_types.rb +184 -0
- data/lib/odata/entity.rb +151 -197
- data/lib/odata/error.rb +175 -32
- data/lib/odata/expand.rb +126 -0
- data/lib/odata/filter/base.rb +74 -0
- data/lib/odata/filter/error.rb +49 -6
- data/lib/odata/filter/parse.rb +44 -36
- data/lib/odata/filter/sequel.rb +136 -67
- data/lib/odata/filter/sequel_function_adapter.rb +148 -0
- data/lib/odata/filter/token.rb +26 -19
- data/lib/odata/filter/tree.rb +113 -63
- data/lib/odata/function_import.rb +168 -0
- data/lib/odata/model_ext.rb +637 -0
- data/lib/odata/navigation_attribute.rb +44 -61
- data/lib/odata/relations.rb +5 -5
- data/lib/odata/select.rb +54 -0
- data/lib/odata/transition.rb +71 -0
- data/lib/odata/url_parameters.rb +128 -37
- data/lib/odata/walker.rb +19 -11
- data/lib/safrano.rb +17 -37
- data/lib/safrano/contract.rb +143 -0
- data/lib/safrano/core.rb +29 -104
- data/lib/safrano/core_ext.rb +13 -0
- data/lib/safrano/deprecation.rb +73 -0
- data/lib/safrano/multipart.rb +39 -43
- data/lib/safrano/rack_app.rb +68 -67
- data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
- data/lib/safrano/request.rb +102 -51
- data/lib/safrano/response.rb +5 -3
- data/lib/safrano/sequel_join_by_paths.rb +2 -2
- data/lib/safrano/service.rb +264 -220
- data/lib/safrano/version.rb +3 -1
- data/lib/sequel/plugins/join_by_paths.rb +17 -29
- metadata +34 -12
@@ -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
|
data/lib/safrano/request.rb
CHANGED
@@ -1,13 +1,15 @@
|
|
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
|
8
10
|
class Request < Rack::Request
|
9
11
|
HEADER_PARAM = /\s*[\w.]+=(?:[\w.]+|"(?:[^"\\]|\\.)*")?\s*/.freeze
|
10
|
-
HEADER_VAL_RAW = '(?:\w+|\*)\/(?:\w+(?:\.|\-|\+)?|\*)*'
|
12
|
+
HEADER_VAL_RAW = '(?:\w+|\*)\/(?:\w+(?:\.|\-|\+)?|\*)*'
|
11
13
|
HEADER_VAL_WITH_PAR = /(?:#{HEADER_VAL_RAW})\s*(?:;#{HEADER_PARAM})*/.freeze
|
12
14
|
ON_CGST_ERROR = (proc { |r| raise(Sequel::Rollback) if r.in_changeset })
|
13
15
|
|
@@ -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
|
@@ -108,13 +113,6 @@ module OData
|
|
108
113
|
end
|
109
114
|
end
|
110
115
|
|
111
|
-
def uribase
|
112
|
-
return @uribase if @uribase
|
113
|
-
|
114
|
-
@uribase =
|
115
|
-
"#{env['rack.url_scheme']}://#{env['HTTP_HOST']}#{service.xpath_prefix}"
|
116
|
-
end
|
117
|
-
|
118
116
|
def accept?(type)
|
119
117
|
preferred_type(type).to_s.include?(type)
|
120
118
|
end
|
@@ -134,14 +132,14 @@ module OData
|
|
134
132
|
|
135
133
|
def with_media_data
|
136
134
|
if (filename = @env['HTTP_SLUG'])
|
137
|
-
|
138
|
-
yield @env['rack.input'],
|
135
|
+
|
136
|
+
yield @env['rack.input'],
|
139
137
|
content_type.split(';').first,
|
140
138
|
Rfc2047.decode(filename)
|
141
139
|
|
142
140
|
else
|
143
141
|
ON_CGST_ERROR.call(self)
|
144
|
-
|
142
|
+
[400, EMPTY_HASH, ['File upload error: Missing SLUG']]
|
145
143
|
end
|
146
144
|
end
|
147
145
|
|
@@ -152,56 +150,109 @@ module OData
|
|
152
150
|
data = JSON.parse(body.read)
|
153
151
|
rescue JSON::ParserError => e
|
154
152
|
ON_CGST_ERROR.call(self)
|
155
|
-
return [400,
|
156
|
-
|
153
|
+
return [400, EMPTY_HASH, ['JSON Parser Error while parsing payload : ',
|
154
|
+
e.message]]
|
157
155
|
end
|
158
156
|
|
159
157
|
yield data
|
160
158
|
|
161
159
|
else # TODO: other formats
|
162
160
|
|
163
|
-
[415,
|
161
|
+
[415, EMPTY_HASH, EMPTY_ARRAY]
|
164
162
|
end
|
165
163
|
end
|
166
164
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
OData::MIN_DATASERVICE_VERSION
|
181
|
-
end
|
182
|
-
return OData::BadRequestError if minv.nil?
|
183
|
-
# client request an too new version --> 501
|
184
|
-
return OData::NotImplementedError if minv > OData::MAX_DATASERVICE_VERSION
|
185
|
-
return OData::BadRequestError if minv > maxv
|
186
|
-
|
187
|
-
v = if (rqv = env['HTTP_DATASERVICEVERSION'])
|
188
|
-
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
|
189
178
|
else
|
190
|
-
|
179
|
+
Contract.valid(maxv)
|
191
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
|
192
189
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
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) ||
|
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
|
205
256
|
end
|
206
257
|
end
|
207
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)
|
@@ -34,7 +36,7 @@ module OData
|
|
34
36
|
|
35
37
|
if drop_body?
|
36
38
|
close
|
37
|
-
result =
|
39
|
+
result = EMPTY_ARRAY
|
38
40
|
end
|
39
41
|
|
40
42
|
if calculate_content_length?
|
data/lib/safrano/service.rb
CHANGED
@@ -1,184 +1,118 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'rexml/document'
|
4
|
-
require 'odata/relations
|
5
|
-
require 'odata/batch
|
6
|
-
require 'odata/
|
7
|
-
|
8
|
-
|
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
|
9
14
|
# this module has all methods related to expand/defered output preparation
|
10
15
|
# and will be included in Service class
|
11
16
|
module ExpandHandler
|
12
|
-
PATH_SPLITTER = %r{\A(\w+)
|
13
|
-
|
14
|
-
|
15
|
-
end
|
16
|
-
|
17
|
-
# split expand argument into first and rest part
|
18
|
-
def split_entity_expand_arg(exp_one)
|
19
|
-
if exp_one.include?('/')
|
20
|
-
m = PATH_SPLITTER.match(exp_one)
|
21
|
-
cur_exp = m[1].strip
|
22
|
-
rest_exp = m[2]
|
23
|
-
# TODO: check errorhandling
|
24
|
-
raise OData::ServerError if cur_exp.nil?
|
17
|
+
PATH_SPLITTER = %r{\A(\w+)/?(.*)\z}.freeze
|
18
|
+
DEFERRED = '__deferred'
|
19
|
+
URI = 'uri'
|
25
20
|
|
26
|
-
|
27
|
-
|
28
|
-
k = exp_one.strip.to_sym
|
29
|
-
rest_exp = nil
|
30
|
-
end
|
31
|
-
yield k, rest_exp
|
21
|
+
def get_deferred_odata_h(entity_uri:, attrib:)
|
22
|
+
{ DEFERRED => { URI => "#{entity_uri}/#{attrib}" } }
|
32
23
|
end
|
33
24
|
|
34
25
|
# default v2
|
35
26
|
# overriden in ServiceV1
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
end
|
42
|
-
if icount
|
43
|
-
{ 'results' => res, '__count' => icount }
|
44
|
-
else
|
45
|
-
{ 'results' => res }
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
# for expand 1..n nav attributes
|
50
|
-
# actually same as v1 get_coll_odata_h
|
51
|
-
# def get_expandcoll_odata_h(array:, expand: nil, uribase:, icount: nil)
|
52
|
-
# array.map do |w|
|
53
|
-
# get_entity_odata_h(entity: w,
|
54
|
-
# expand: expand,
|
55
|
-
# uribase: uribase)
|
56
|
-
# end
|
57
|
-
# end
|
58
|
-
|
59
|
-
# handle a single expand
|
60
|
-
def handle_entity_expand_one(entity:, exp_one:, nav_values_h:, nav_coll_h:,
|
61
|
-
uribase:)
|
62
|
-
|
63
|
-
|
64
|
-
split_entity_expand_arg(exp_one) do |first, rest_exp|
|
65
|
-
if ( entity.nav_values.has_key?(first) )
|
66
|
-
if (enval = entity.nav_values[first])
|
67
|
-
nav_values_h[first.to_s] = get_entity_odata_h(entity: enval,
|
68
|
-
expand: rest_exp,
|
69
|
-
uribase: uribase)
|
70
|
-
else
|
71
|
-
# FK is NULL --> nav_value is nil --> return empty json
|
72
|
-
nav_values_h[first.to_s] = {}
|
73
|
-
end
|
74
|
-
elsif (encoll = entity.nav_coll[first])
|
75
|
-
# nav attributes that are a collection (x..n)
|
76
|
-
nav_coll_h[first.to_s] = get_coll_odata_h(array: encoll,
|
77
|
-
expand: rest_exp,
|
78
|
-
uribase: uribase)
|
79
|
-
# nav_coll_h[first.to_s] = get_expandcoll_odata_h(array: encoll,
|
80
|
-
# expand: rest_exp,
|
81
|
-
# uribase: uribase)
|
82
|
-
|
83
|
-
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
def handle_entity_expand(entity:, expand:, nav_values_h:,
|
89
|
-
nav_coll_h:, uribase:)
|
90
|
-
expand.strip!
|
91
|
-
explist = expand.split(',')
|
92
|
-
# handle multiple expands
|
93
|
-
explist.each do |exp|
|
94
|
-
handle_entity_expand_one(entity: entity,
|
95
|
-
exp_one: exp,
|
96
|
-
nav_values_h: nav_values_h,
|
97
|
-
nav_coll_h: nav_coll_h,
|
98
|
-
uribase: uribase)
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
def handle_entity_deferred_attribs(entity:, nav_values_h:,
|
103
|
-
nav_coll_h:, uribase:)
|
104
|
-
entity.nav_values.each_key do |ksy|
|
105
|
-
ks = ksy.to_s
|
106
|
-
next if nav_values_h.key?(ks)
|
107
|
-
|
108
|
-
nav_values_h[ks] = get_deferred_odata_h(entity: entity,
|
109
|
-
attrib: ks, uribase: uribase)
|
110
|
-
end
|
111
|
-
entity.nav_coll.each_key do |ksy|
|
112
|
-
ks = ksy.to_s
|
113
|
-
next if nav_coll_h.key?(ks)
|
114
|
-
|
115
|
-
nav_coll_h[ks] = get_deferred_odata_h(entity: entity, attrib: ks,
|
116
|
-
uribase: uribase)
|
27
|
+
RESULTS_K = 'results'
|
28
|
+
COUNT_K = '__count'
|
29
|
+
def get_coll_odata_h(array:, template:, icount: nil)
|
30
|
+
array.map! do |w|
|
31
|
+
get_entity_odata_h(entity: w, template: template)
|
117
32
|
end
|
33
|
+
icount ? { RESULTS_K => array, COUNT_K => icount } : { RESULTS_K => array }
|
118
34
|
end
|
119
35
|
|
120
36
|
# handle $links ... Note: $expand seems to be ignored when $links
|
121
37
|
# are requested
|
122
|
-
def get_entity_odata_link_h(entity
|
123
|
-
{ uri: entity.uri
|
124
|
-
end
|
38
|
+
def get_entity_odata_link_h(entity:)
|
39
|
+
{ uri: entity.uri }
|
40
|
+
end
|
41
|
+
|
42
|
+
EMPTYH = {}.freeze
|
43
|
+
METADATA_K = '__metadata'
|
44
|
+
def get_entity_odata_h(entity:, template:)
|
45
|
+
# start with metadata
|
46
|
+
hres = { METADATA_K => entity.metadata_h }
|
47
|
+
|
48
|
+
template.each do |elmt, arg|
|
49
|
+
case elmt
|
50
|
+
when :all_values
|
51
|
+
hres.merge! entity.casted_values
|
52
|
+
|
53
|
+
when :selected_vals
|
54
|
+
hres.merge! entity.casted_values(arg)
|
55
|
+
|
56
|
+
when :expand_e
|
57
|
+
|
58
|
+
arg.each do |attr, templ|
|
59
|
+
enval = entity.send(attr)
|
60
|
+
hres[attr] = if enval
|
61
|
+
get_entity_odata_h(entity: enval, template: templ)
|
62
|
+
else
|
63
|
+
# FK is NULL --> nav_value is nil --> return empty json
|
64
|
+
EMPTYH
|
65
|
+
end
|
66
|
+
end
|
125
67
|
|
126
|
-
|
127
|
-
|
128
|
-
|
68
|
+
when :expand_c
|
69
|
+
arg.each do |attr, templ|
|
70
|
+
next unless (encoll = entity.send(attr))
|
129
71
|
|
130
|
-
|
131
|
-
|
72
|
+
# nav attributes that are a collection (x..n)
|
73
|
+
hres[attr] = get_coll_odata_h(array: encoll, template: templ)
|
74
|
+
# else error ?
|
75
|
+
end
|
132
76
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
77
|
+
when :deferr
|
78
|
+
euri = entity.uri
|
79
|
+
arg.each do |attr|
|
80
|
+
hres[attr] = get_deferred_odata_h(entity_uri: euri, attrib: attr)
|
81
|
+
end
|
82
|
+
end
|
139
83
|
end
|
140
|
-
|
141
|
-
# handle not expanded (deferred) nav attributes
|
142
|
-
handle_entity_deferred_attribs(entity: entity,
|
143
|
-
nav_values_h: nav_values_h,
|
144
|
-
nav_coll_h: nav_coll_h,
|
145
|
-
uribase: uribase)
|
146
|
-
# merge ...
|
147
|
-
hres.merge!(nav_values_h)
|
148
|
-
hres.merge!(nav_coll_h)
|
149
|
-
|
150
84
|
hres
|
151
85
|
end
|
152
86
|
end
|
153
87
|
end
|
154
88
|
|
155
|
-
module
|
89
|
+
module Safrano
|
156
90
|
# xml namespace constants needed for the output of XML service and
|
157
91
|
# and metadata
|
158
92
|
module XMLNS
|
159
|
-
MSFT_ADO = 'http://schemas.microsoft.com/ado'
|
160
|
-
MSFT_ADO_2009_EDM = "#{MSFT_ADO}/2009/11/edm"
|
161
|
-
MSFT_ADO_2007_EDMX = "#{MSFT_ADO}/2007/06/edmx"
|
162
|
-
MSFT_ADO_2007_META = MSFT_ADO
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
W3_2007_APP = 'http://www.w3.org/2007/app'.freeze
|
93
|
+
MSFT_ADO = 'http://schemas.microsoft.com/ado'
|
94
|
+
MSFT_ADO_2009_EDM = "#{MSFT_ADO}/2009/11/edm"
|
95
|
+
MSFT_ADO_2007_EDMX = "#{MSFT_ADO}/2007/06/edmx"
|
96
|
+
MSFT_ADO_2007_META = "#{MSFT_ADO}/2007/08/dataservices/metadata"
|
97
|
+
|
98
|
+
W3_2005_ATOM = 'http://www.w3.org/2005/Atom'
|
99
|
+
W3_2007_APP = 'http://www.w3.org/2007/app'
|
167
100
|
end
|
168
101
|
end
|
169
102
|
|
170
103
|
# Link to Model
|
171
|
-
module
|
172
|
-
MAX_DATASERVICE_VERSION = '2'
|
173
|
-
MIN_DATASERVICE_VERSION = '1'
|
104
|
+
module Safrano
|
105
|
+
MAX_DATASERVICE_VERSION = '2'
|
106
|
+
MIN_DATASERVICE_VERSION = '1'
|
107
|
+
CV_MAX_DATASERVICE_VERSION = Contract.valid(MAX_DATASERVICE_VERSION).freeze
|
108
|
+
CV_MIN_DATASERVICE_VERSION = Contract.valid(MIN_DATASERVICE_VERSION).freeze
|
174
109
|
include XMLNS
|
175
110
|
# Base class for service. Subclass will be for V1, V2 etc...
|
176
111
|
class ServiceBase
|
177
112
|
include Safrano
|
178
113
|
include ExpandHandler
|
179
114
|
|
180
|
-
XML_PREAMBLE =
|
181
|
-
"\r\n".freeze
|
115
|
+
XML_PREAMBLE = %Q(<?xml version="1.0" encoding="utf-8" standalone="yes"?>\r\n)
|
182
116
|
|
183
117
|
# This is just a hash of entity Set Names to the corresponding Class
|
184
118
|
# Example
|
@@ -196,10 +130,14 @@ module OData
|
|
196
130
|
attr_accessor :xname
|
197
131
|
attr_accessor :xnamespace
|
198
132
|
attr_accessor :xpath_prefix
|
199
|
-
|
133
|
+
attr_accessor :xserver_url
|
134
|
+
attr_accessor :uribase
|
200
135
|
attr_accessor :meta
|
201
136
|
attr_accessor :batch_handler
|
202
137
|
attr_accessor :relman
|
138
|
+
attr_accessor :complex_types
|
139
|
+
attr_accessor :function_imports
|
140
|
+
|
203
141
|
# Instance attributes for specialized Version specific Instances
|
204
142
|
attr_accessor :v1
|
205
143
|
attr_accessor :v2
|
@@ -213,36 +151,31 @@ module OData
|
|
213
151
|
# because of the version subclasses that dont use "super" initialise
|
214
152
|
# (todo: why not??)
|
215
153
|
@meta = ServiceMeta.new(self)
|
216
|
-
@batch_handler =
|
217
|
-
@relman =
|
154
|
+
@batch_handler = Safrano::Batch::DisabledHandler.new
|
155
|
+
@relman = Safrano::RelationManager.new
|
156
|
+
@complex_types = Set.new
|
157
|
+
@function_imports = {}
|
218
158
|
@cmap = {}
|
219
159
|
instance_eval(&block) if block_given?
|
220
160
|
end
|
221
161
|
|
222
|
-
DATASERVICEVERSION_RGX = /\A([1234])(?:\.0);*\w*\z/.freeze
|
223
162
|
TRAILING_SLASH = %r{/\z}.freeze
|
224
163
|
DEFAULT_PATH_PREFIX = '/'
|
225
|
-
|
226
|
-
# input is the DataServiceVersion request header string, eg.
|
227
|
-
# '2.0;blabla' ---> Version -> 2
|
228
|
-
def self.parse_data_service_version(inp)
|
229
|
-
m = DATASERVICEVERSION_RGX.match(inp)
|
230
|
-
m[1] if m
|
231
|
-
end
|
164
|
+
DEFAULT_SERVER_URL = 'http://localhost:9494'
|
232
165
|
|
233
166
|
def enable_batch
|
234
|
-
@batch_handler =
|
167
|
+
@batch_handler = Safrano::Batch::EnabledHandler.new
|
235
168
|
(@v1.batch_handler = @batch_handler) if @v1
|
236
169
|
(@v2.batch_handler = @batch_handler) if @v2
|
237
170
|
end
|
238
171
|
|
239
172
|
def enable_v1_service
|
240
|
-
@v1 =
|
173
|
+
@v1 = Safrano::ServiceV1.new
|
241
174
|
copy_attribs_to @v1
|
242
175
|
end
|
243
176
|
|
244
177
|
def enable_v2_service
|
245
|
-
@v2 =
|
178
|
+
@v2 = Safrano::ServiceV2.new
|
246
179
|
copy_attribs_to @v2
|
247
180
|
end
|
248
181
|
|
@@ -264,59 +197,91 @@ module OData
|
|
264
197
|
(@v1.xpath_prefix = @xpath_prefix) if @v1
|
265
198
|
(@v2.xpath_prefix = @xpath_prefix) if @v2
|
266
199
|
end
|
200
|
+
|
201
|
+
def server_url(surl)
|
202
|
+
@xserver_url = surl.sub(TRAILING_SLASH, '')
|
203
|
+
(@v1.xserver_url = @xserver_url) if @v1
|
204
|
+
(@v2.xserver_url = @xserver_url) if @v2
|
205
|
+
end
|
206
|
+
|
267
207
|
# end public API
|
268
208
|
|
209
|
+
def set_uribase
|
210
|
+
@uribase = if @xpath_prefix.empty?
|
211
|
+
@xserver_url
|
212
|
+
elsif @xpath_prefix[0] == '/'
|
213
|
+
"#{@xserver_url}#{@xpath_prefix}"
|
214
|
+
else
|
215
|
+
"#{@xserver_url}/#{@xpath_prefix}"
|
216
|
+
end
|
217
|
+
(@v1.uribase = @uribase) if @v1
|
218
|
+
(@v2.uribase = @uribase) if @v2
|
219
|
+
end
|
220
|
+
|
269
221
|
def copy_attribs_to(other)
|
270
222
|
other.cmap = @cmap
|
271
223
|
other.collections = @collections
|
224
|
+
other.allowed_transitions = @allowed_transitions
|
272
225
|
other.xtitle = @xtitle
|
273
226
|
other.xname = @xname
|
274
227
|
other.xnamespace = @xnamespace
|
275
228
|
other.xpath_prefix = @xpath_prefix
|
229
|
+
other.xserver_url = @xserver_url
|
230
|
+
other.uribase = @uribase
|
276
231
|
other.meta = ServiceMeta.new(other) # hum ... #todo: versions as well ?
|
277
232
|
other.relman = @relman
|
278
233
|
other.batch_handler = @batch_handler
|
234
|
+
other.complex_types = @complex_types
|
235
|
+
other.function_imports = @function_imports
|
279
236
|
other
|
280
237
|
end
|
281
238
|
|
239
|
+
# this is a central place. We extend Sequel models with OData functionality
|
240
|
+
# The included/extended modules depends on the properties(eg, pks, field types) of the model
|
241
|
+
# we differentiate
|
242
|
+
# * Single/Multi PK
|
243
|
+
# * Media/Non-Media entity
|
244
|
+
# Putting this logic here in modules loaded once on start shall result in less runtime overhead
|
282
245
|
def register_model(modelklass, entity_set_name = nil, is_media = false)
|
283
246
|
# check that the provided klass is a Sequel Model
|
284
|
-
unless modelklass.is_a? Sequel::Model::ClassMethods
|
285
|
-
raise OData::API::ModelNameError, modelklass
|
286
|
-
end
|
287
247
|
|
288
|
-
|
248
|
+
raise(Safrano::API::ModelNameError, modelklass) unless modelklass.is_a? Sequel::Model::ClassMethods
|
249
|
+
|
250
|
+
if modelklass.ancestors.include? Safrano::Entity
|
289
251
|
# modules were already added previously;
|
290
252
|
# cleanup state to avoid having data from previous calls
|
291
253
|
# mostly usefull for testing (eg API)
|
292
254
|
modelklass.reset
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
modelklass.include OData::EntitySinglePK
|
300
|
-
end
|
255
|
+
elsif modelklass.primary_key.is_a?(Array) # first API call... (normal non-testing case)
|
256
|
+
modelklass.extend Safrano::EntityClassMultiPK
|
257
|
+
modelklass.include Safrano::EntityMultiPK
|
258
|
+
else
|
259
|
+
modelklass.extend Safrano::EntityClassSinglePK
|
260
|
+
modelklass.include Safrano::EntitySinglePK
|
301
261
|
end
|
262
|
+
|
302
263
|
# Media/Non-media
|
303
264
|
if is_media
|
304
|
-
modelklass.extend
|
265
|
+
modelklass.extend Safrano::EntityClassMedia
|
305
266
|
# set default media handler . Can be overridden later with the
|
306
267
|
# "use HandlerKlass, options" API
|
307
268
|
|
308
269
|
modelklass.set_default_media_handler
|
309
270
|
modelklass.api_check_media_fields
|
310
|
-
modelklass.include
|
271
|
+
modelklass.include Safrano::MediaEntity
|
311
272
|
else
|
312
|
-
modelklass.extend
|
313
|
-
modelklass.include
|
273
|
+
modelklass.extend Safrano::EntityClassNonMedia
|
274
|
+
modelklass.include Safrano::NonMediaEntity
|
314
275
|
end
|
315
276
|
|
316
277
|
modelklass.prepare_pk
|
317
278
|
modelklass.prepare_fields
|
318
279
|
esname = (entity_set_name || modelklass).to_s.freeze
|
319
|
-
|
280
|
+
serv_namespace = @xnamespace
|
281
|
+
modelklass.instance_eval do
|
282
|
+
@entity_set_name = esname
|
283
|
+
@namespace = serv_namespace
|
284
|
+
end
|
320
285
|
@cmap[esname] = modelklass
|
321
286
|
set_collections_sorted(@cmap.values)
|
322
287
|
end
|
@@ -337,6 +302,23 @@ module OData
|
|
337
302
|
modelklass.deferred_iblock = block if block_given?
|
338
303
|
end
|
339
304
|
|
305
|
+
def publish_complex_type(ctklass)
|
306
|
+
# check that the provided klass is a Safrano ComplexType
|
307
|
+
|
308
|
+
raise(Safrano::API::ComplexTypeNameError, ctklass) unless ctklass.superclass == Safrano::ComplexType
|
309
|
+
|
310
|
+
serv_namespace = @xnamespace
|
311
|
+
ctklass.instance_eval { @namespace = serv_namespace }
|
312
|
+
|
313
|
+
@complex_types.add ctklass
|
314
|
+
end
|
315
|
+
|
316
|
+
def function_import(name)
|
317
|
+
funcimp = Safrano::FunctionImport(name)
|
318
|
+
@function_imports[name] = funcimp
|
319
|
+
funcimp
|
320
|
+
end
|
321
|
+
|
340
322
|
def cmap=(imap)
|
341
323
|
@cmap = imap
|
342
324
|
set_collections_sorted(@cmap.values)
|
@@ -347,9 +329,7 @@ module OData
|
|
347
329
|
# example: CrewMember must be matched before Crew otherwise we get error
|
348
330
|
def set_collections_sorted(coll_data)
|
349
331
|
@collections = coll_data
|
350
|
-
if @collections
|
351
|
-
@collections.sort_by! { |klass| klass.entity_set_name.size }.reverse!
|
352
|
-
end
|
332
|
+
@collections.sort_by! { |klass| klass.entity_set_name.size }.reverse! if @collections
|
353
333
|
@collections
|
354
334
|
end
|
355
335
|
|
@@ -370,10 +350,32 @@ module OData
|
|
370
350
|
# set default path prefix if path_prefix was not called
|
371
351
|
path_prefix(DEFAULT_PATH_PREFIX) unless @xpath_prefix
|
372
352
|
|
353
|
+
# set default server url if server_url was not called
|
354
|
+
server_url(DEFAULT_SERVER_URL) unless @xserver_url
|
355
|
+
|
356
|
+
set_uribase
|
357
|
+
|
373
358
|
@collections.each(&:finalize_publishing)
|
374
|
-
|
375
|
-
#finalize the
|
376
|
-
@collections.each
|
359
|
+
|
360
|
+
# finalize the uri's and include NoMappingBeforeOutput or MappingBeforeOutput as needed
|
361
|
+
@collections.each do |klass|
|
362
|
+
klass.build_uri(@uribase)
|
363
|
+
klass.include(klass.time_cols.empty? ? Safrano::NoMappingBeforeOutput : Safrano::MappingBeforeOutput)
|
364
|
+
end
|
365
|
+
|
366
|
+
# build allowed transitions (requires that @collections are filled and sorted for having a
|
367
|
+
# correct base_url_regexp)
|
368
|
+
build_allowed_transitions
|
369
|
+
|
370
|
+
# mixin adapter specific modules where needed
|
371
|
+
case Sequel::Model.db.adapter_scheme
|
372
|
+
when :postgres
|
373
|
+
Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreePostgres
|
374
|
+
when :sqlite
|
375
|
+
Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreeSqlite
|
376
|
+
else
|
377
|
+
Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreeDefault
|
378
|
+
end
|
377
379
|
end
|
378
380
|
|
379
381
|
def execute_deferred_iblocks
|
@@ -389,19 +391,24 @@ module OData
|
|
389
391
|
@collections.map(&:entity_set_name).join('|')
|
390
392
|
end
|
391
393
|
|
394
|
+
def base_url_func_regexp
|
395
|
+
@function_imports.keys.join('|')
|
396
|
+
end
|
397
|
+
|
392
398
|
def service
|
393
399
|
hres = {}
|
394
400
|
hres['d'] = { 'EntitySets' => @collections.map(&:type_name) }
|
395
401
|
hres
|
396
402
|
end
|
397
403
|
|
398
|
-
def service_xml(
|
404
|
+
def service_xml(_req)
|
399
405
|
doc = REXML::Document.new
|
400
406
|
# separator required ? ?
|
401
|
-
root = doc.add_element('service', 'xml:base' =>
|
407
|
+
root = doc.add_element('service', 'xml:base' => @uribase)
|
402
408
|
|
403
409
|
root.add_namespace('xmlns:atom', XMLNS::W3_2005_ATOM)
|
404
410
|
root.add_namespace('xmlns:app', XMLNS::W3_2007_APP)
|
411
|
+
|
405
412
|
# this generates the main xmlns attribute
|
406
413
|
root.add_namespace(XMLNS::W3_2007_APP)
|
407
414
|
wp = root.add_element 'workspace'
|
@@ -412,7 +419,7 @@ module OData
|
|
412
419
|
@collections.each do |klass|
|
413
420
|
col = wp.add_element('collection', 'href' => klass.entity_set_name)
|
414
421
|
ct = col.add_element('atom:title')
|
415
|
-
ct.text = klass.
|
422
|
+
ct.text = klass.entity_set_name
|
416
423
|
end
|
417
424
|
|
418
425
|
XML_PREAMBLE + doc.to_pretty_xml
|
@@ -421,10 +428,18 @@ module OData
|
|
421
428
|
def add_metadata_xml_entity_type(schema)
|
422
429
|
@collections.each do |klass|
|
423
430
|
enty = klass.add_metadata_rexml(schema)
|
424
|
-
klass.add_metadata_navs_rexml(enty, @relman
|
431
|
+
klass.add_metadata_navs_rexml(enty, @relman)
|
425
432
|
end
|
426
433
|
end
|
427
434
|
|
435
|
+
def add_metadata_xml_complex_types(schema)
|
436
|
+
@complex_types.each { |ctklass| ctklass.add_metadata_rexml(schema) }
|
437
|
+
end
|
438
|
+
|
439
|
+
def add_metadata_xml_function_imports(ec)
|
440
|
+
@function_imports.each_value { |func| func.add_metadata_rexml(ec) }
|
441
|
+
end
|
442
|
+
|
428
443
|
def add_metadata_xml_associations(schema)
|
429
444
|
@relman.each_rel do |rel|
|
430
445
|
rel.with_metadata_info(@xnamespace) do |name, bdinfo|
|
@@ -447,7 +462,7 @@ module OData
|
|
447
462
|
# 3.a Entity set's
|
448
463
|
ec.add_element('EntitySet',
|
449
464
|
'Name' => klass.entity_set_name,
|
450
|
-
'EntityType' =>
|
465
|
+
'EntityType' => klass.type_name)
|
451
466
|
end
|
452
467
|
# 3.b Association set's
|
453
468
|
@relman.each_rel do |rel|
|
@@ -461,6 +476,9 @@ module OData
|
|
461
476
|
assoc.add_element('End', assoend)
|
462
477
|
end
|
463
478
|
end
|
479
|
+
|
480
|
+
# 4 function imports
|
481
|
+
add_metadata_xml_function_imports(ec)
|
464
482
|
end
|
465
483
|
|
466
484
|
def metadata_xml(_req)
|
@@ -494,9 +512,12 @@ module OData
|
|
494
512
|
schema = serv.add_element('Schema',
|
495
513
|
'Namespace' => @xnamespace,
|
496
514
|
'xmlns' => XMLNS::MSFT_ADO_2009_EDM)
|
497
|
-
# 1. all EntityType
|
515
|
+
# 1. a. all EntityType
|
498
516
|
add_metadata_xml_entity_type(schema)
|
499
517
|
|
518
|
+
# 1. b. all ComplexType
|
519
|
+
add_metadata_xml_complex_types(schema)
|
520
|
+
|
500
521
|
# 2. Associations
|
501
522
|
add_metadata_xml_associations(schema)
|
502
523
|
|
@@ -508,20 +529,45 @@ module OData
|
|
508
529
|
|
509
530
|
# methods related to transitions to next state (cf. walker)
|
510
531
|
module Transitions
|
511
|
-
DOLLAR_ID_REGEXP = Regexp.new('\A\/\$')
|
532
|
+
DOLLAR_ID_REGEXP = Regexp.new('\A\/\$')
|
533
|
+
ALLOWED_TRANSITIONS_FIXED = [
|
534
|
+
Safrano::TransitionEnd,
|
535
|
+
Safrano::TransitionMetadata,
|
536
|
+
Safrano::TransitionBatch,
|
537
|
+
Safrano::TransitionContentId
|
538
|
+
].freeze
|
539
|
+
|
540
|
+
def build_allowed_transitions
|
541
|
+
@allowed_transitions = if @function_imports.empty?
|
542
|
+
(ALLOWED_TRANSITIONS_FIXED + [
|
543
|
+
Safrano::Transition.new(%r{\A/(#{base_url_regexp})(.*)},
|
544
|
+
trans: 'transition_collection')
|
545
|
+
]).freeze
|
546
|
+
else
|
547
|
+
(ALLOWED_TRANSITIONS_FIXED + [
|
548
|
+
Safrano::Transition.new(%r{\A/(#{base_url_regexp})(.*)},
|
549
|
+
trans: 'transition_collection'),
|
550
|
+
Safrano::Transition.new(%r{\A/(#{base_url_func_regexp})(.*)},
|
551
|
+
trans: 'transition_service_op')
|
552
|
+
]).freeze
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
512
556
|
def allowed_transitions
|
513
|
-
@allowed_transitions
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
Safrano::Transition.new(%r{\A/(#{base_url_regexp})(.*)},
|
519
|
-
trans: 'transition_collection')
|
520
|
-
]
|
557
|
+
@allowed_transitions
|
558
|
+
end
|
559
|
+
|
560
|
+
def thread_safe_collection(collklass)
|
561
|
+
Safrano::OData::Collection.new(collklass)
|
521
562
|
end
|
522
563
|
|
523
564
|
def transition_collection(match_result)
|
524
|
-
[@cmap[match_result[1]], :run] if match_result[1]
|
565
|
+
[thread_safe_collection(@cmap[match_result[1]]), :run] if match_result[1]
|
566
|
+
# [@cmap[match_result[1]], :run] if match_result[1]
|
567
|
+
end
|
568
|
+
|
569
|
+
def transition_service_op(match_result)
|
570
|
+
[@function_imports[match_result[1]], :run] if match_result[1]
|
525
571
|
end
|
526
572
|
|
527
573
|
def transition_batch(_match_result)
|
@@ -537,7 +583,7 @@ module OData
|
|
537
583
|
end
|
538
584
|
|
539
585
|
def transition_end(_match_result)
|
540
|
-
|
586
|
+
Safrano::Transition::RESULT_END
|
541
587
|
end
|
542
588
|
end
|
543
589
|
|
@@ -545,14 +591,11 @@ module OData
|
|
545
591
|
|
546
592
|
def odata_get(req)
|
547
593
|
if req.accept?(APPXML)
|
548
|
-
#
|
549
|
-
#
|
550
|
-
|
551
|
-
# identified with the "application/atomsvc+xml" media type (see
|
552
|
-
# [RFC5023] section 8).
|
553
|
-
[200, CT_ATOMXML, [service_xml(req)]]
|
594
|
+
# OData V2 reference service implementations are returning app-xml-u8
|
595
|
+
# so we do
|
596
|
+
[200, CT_APPXML, [service_xml(req)]]
|
554
597
|
else
|
555
|
-
# this is returned by http://services.odata.org/V2/OData/
|
598
|
+
# this is returned by http://services.odata.org/V2/OData/Safrano.svc
|
556
599
|
415
|
557
600
|
end
|
558
601
|
end
|
@@ -564,22 +607,22 @@ module OData
|
|
564
607
|
@data_service_version = '1.0'
|
565
608
|
end
|
566
609
|
|
567
|
-
def get_coll_odata_links_h(array:,
|
610
|
+
def get_coll_odata_links_h(array:, icount: nil)
|
568
611
|
array.map do |w|
|
569
|
-
get_entity_odata_link_h(entity: w
|
612
|
+
get_entity_odata_link_h(entity: w)
|
570
613
|
end
|
571
614
|
end
|
572
615
|
|
573
|
-
def get_coll_odata_h(array:,
|
574
|
-
array.map do |w|
|
616
|
+
def get_coll_odata_h(array:, template:, icount: nil)
|
617
|
+
array.map! do |w|
|
575
618
|
get_entity_odata_h(entity: w,
|
576
|
-
|
577
|
-
uribase: uribase)
|
619
|
+
template: template)
|
578
620
|
end
|
621
|
+
array
|
579
622
|
end
|
580
623
|
|
581
624
|
def get_emptycoll_odata_h
|
582
|
-
|
625
|
+
EMPTY_HASH_IN_ARY
|
583
626
|
end
|
584
627
|
end
|
585
628
|
|
@@ -589,36 +632,37 @@ module OData
|
|
589
632
|
@data_service_version = '2.0'
|
590
633
|
end
|
591
634
|
|
592
|
-
def get_coll_odata_links_h(array:,
|
635
|
+
def get_coll_odata_links_h(array:, icount: nil)
|
593
636
|
res = array.map do |w|
|
594
|
-
get_entity_odata_link_h(entity: w
|
637
|
+
get_entity_odata_link_h(entity: w)
|
595
638
|
end
|
596
639
|
if icount
|
597
|
-
{
|
640
|
+
{ RESULTS_K => res, COUNT_K => icount }
|
598
641
|
else
|
599
|
-
{
|
642
|
+
{ RESULTS_K => res }
|
600
643
|
end
|
601
644
|
end
|
602
645
|
|
603
646
|
def get_emptycoll_odata_h
|
604
|
-
{
|
647
|
+
{ RESULTS_K => EMPTY_HASH_IN_ARY }
|
605
648
|
end
|
606
649
|
end
|
607
650
|
|
608
651
|
# a virtual entity for the service metadata
|
609
652
|
class ServiceMeta
|
610
653
|
attr_accessor :service
|
654
|
+
|
611
655
|
def initialize(service)
|
612
656
|
@service = service
|
613
657
|
end
|
614
658
|
|
659
|
+
ALLOWED_TRANSITIONS_FIXED = [Safrano::TransitionEnd].freeze
|
615
660
|
def allowed_transitions
|
616
|
-
|
617
|
-
trans: 'transition_end')]
|
661
|
+
ALLOWED_TRANSITIONS_FIXED
|
618
662
|
end
|
619
663
|
|
620
664
|
def transition_end(_match_result)
|
621
|
-
|
665
|
+
Safrano::Transition::RESULT_END
|
622
666
|
end
|
623
667
|
|
624
668
|
def odata_get(req)
|