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