safrano 0.4.3 → 0.5.1
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 +8 -4
- data/lib/odata/batch.rb +9 -7
- data/lib/odata/collection.rb +139 -642
- data/lib/odata/collection_filter.rb +18 -42
- data/lib/odata/collection_media.rb +111 -54
- data/lib/odata/collection_order.rb +5 -2
- data/lib/odata/common_logger.rb +2 -0
- data/lib/odata/complex_type.rb +196 -0
- data/lib/odata/edm/primitive_types.rb +184 -0
- data/lib/odata/entity.rb +78 -123
- data/lib/odata/error.rb +170 -37
- data/lib/odata/expand.rb +20 -17
- data/lib/odata/filter/base.rb +9 -1
- data/lib/odata/filter/error.rb +43 -27
- data/lib/odata/filter/parse.rb +39 -25
- data/lib/odata/filter/sequel.rb +112 -56
- data/lib/odata/filter/sequel_function_adapter.rb +50 -49
- data/lib/odata/filter/token.rb +21 -18
- data/lib/odata/filter/tree.rb +78 -44
- data/lib/odata/function_import.rb +168 -0
- data/lib/odata/model_ext.rb +641 -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 +18 -10
- data/lib/safrano.rb +18 -38
- data/lib/safrano/contract.rb +141 -0
- data/lib/safrano/core.rb +24 -106
- data/lib/safrano/core_ext.rb +13 -0
- data/lib/safrano/deprecation.rb +73 -0
- data/lib/safrano/multipart.rb +29 -24
- data/lib/safrano/rack_app.rb +62 -63
- data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -1
- data/lib/safrano/request.rb +96 -38
- data/lib/safrano/response.rb +4 -2
- data/lib/safrano/sequel_join_by_paths.rb +2 -2
- data/lib/safrano/service.rb +156 -110
- data/lib/safrano/version.rb +3 -1
- data/lib/sequel/plugins/join_by_paths.rb +6 -19
- metadata +30 -11
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
|
@@ -81,20 +56,17 @@ module OData
|
|
81
56
|
NOCACHE_HDRS = { 'Cache-Control' => 'no-cache',
|
82
57
|
'Expires' => '-1',
|
83
58
|
'Pragma' => 'no-cache' }.freeze
|
84
|
-
DATASERVICEVERSION = 'DataServiceVersion'
|
59
|
+
DATASERVICEVERSION = 'DataServiceVersion'
|
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,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
|
@@ -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) ||
|
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,53 +1,36 @@
|
|
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+)
|
12
|
-
DEFERRED = '__deferred'
|
13
|
-
URI = 'uri'
|
17
|
+
PATH_SPLITTER = %r{\A(\w+)/?(.*)\z}.freeze
|
18
|
+
DEFERRED = '__deferred'
|
19
|
+
URI = 'uri'
|
14
20
|
|
15
21
|
def get_deferred_odata_h(entity_uri:, attrib:)
|
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
|
-
RESULTS_K = 'results'
|
41
|
-
COUNT_K = '__count'
|
27
|
+
RESULTS_K = 'results'
|
28
|
+
COUNT_K = '__count'
|
42
29
|
def get_coll_odata_h(array:, template:, icount: nil)
|
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
|
@@ -57,7 +40,7 @@ module OData
|
|
57
40
|
end
|
58
41
|
|
59
42
|
EMPTYH = {}.freeze
|
60
|
-
METADATA_K = '__metadata'
|
43
|
+
METADATA_K = '__metadata'
|
61
44
|
def get_entity_odata_h(entity:, template:)
|
62
45
|
# start with metadata
|
63
46
|
hres = { METADATA_K => entity.metadata_h }
|
@@ -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,33 +86,33 @@ 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
|
106
|
-
MSFT_ADO = 'http://schemas.microsoft.com/ado'
|
107
|
-
MSFT_ADO_2009_EDM = "#{MSFT_ADO}/2009/11/edm"
|
108
|
-
MSFT_ADO_2007_EDMX = "#{MSFT_ADO}/2007/06/edmx"
|
109
|
-
MSFT_ADO_2007_META = MSFT_ADO
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
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'
|
114
100
|
end
|
115
101
|
end
|
116
102
|
|
117
103
|
# Link to Model
|
118
|
-
module
|
119
|
-
MAX_DATASERVICE_VERSION = '2'
|
120
|
-
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
|
121
109
|
include XMLNS
|
122
110
|
# Base class for service. Subclass will be for V1, V2 etc...
|
123
111
|
class ServiceBase
|
124
112
|
include Safrano
|
125
113
|
include ExpandHandler
|
126
114
|
|
127
|
-
XML_PREAMBLE =
|
128
|
-
"\r\n".freeze
|
115
|
+
XML_PREAMBLE = %(<?xml version="1.0" encoding="utf-8" standalone="yes"?>\r\n)
|
129
116
|
|
130
117
|
# This is just a hash of entity Set Names to the corresponding Class
|
131
118
|
# Example
|
@@ -148,6 +135,9 @@ module OData
|
|
148
135
|
attr_accessor :meta
|
149
136
|
attr_accessor :batch_handler
|
150
137
|
attr_accessor :relman
|
138
|
+
attr_accessor :complex_types
|
139
|
+
attr_accessor :function_imports
|
140
|
+
|
151
141
|
# Instance attributes for specialized Version specific Instances
|
152
142
|
attr_accessor :v1
|
153
143
|
attr_accessor :v2
|
@@ -161,37 +151,31 @@ module OData
|
|
161
151
|
# because of the version subclasses that dont use "super" initialise
|
162
152
|
# (todo: why not??)
|
163
153
|
@meta = ServiceMeta.new(self)
|
164
|
-
@batch_handler =
|
165
|
-
@relman =
|
154
|
+
@batch_handler = Safrano::Batch::DisabledHandler.new
|
155
|
+
@relman = Safrano::RelationManager.new
|
156
|
+
@complex_types = Set.new
|
157
|
+
@function_imports = {}
|
166
158
|
@cmap = {}
|
167
159
|
instance_eval(&block) if block_given?
|
168
160
|
end
|
169
161
|
|
170
|
-
DATASERVICEVERSION_RGX = /\A([1234])(?:\.0);*\w*\z/.freeze
|
171
162
|
TRAILING_SLASH = %r{/\z}.freeze
|
172
|
-
DEFAULT_PATH_PREFIX = '/'
|
163
|
+
DEFAULT_PATH_PREFIX = '/'
|
173
164
|
DEFAULT_SERVER_URL = 'http://localhost:9494'
|
174
165
|
|
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
|
181
|
-
|
182
166
|
def enable_batch
|
183
|
-
@batch_handler =
|
167
|
+
@batch_handler = Safrano::Batch::EnabledHandler.new
|
184
168
|
(@v1.batch_handler = @batch_handler) if @v1
|
185
169
|
(@v2.batch_handler = @batch_handler) if @v2
|
186
170
|
end
|
187
171
|
|
188
172
|
def enable_v1_service
|
189
|
-
@v1 =
|
173
|
+
@v1 = Safrano::ServiceV1.new
|
190
174
|
copy_attribs_to @v1
|
191
175
|
end
|
192
176
|
|
193
177
|
def enable_v2_service
|
194
|
-
@v2 =
|
178
|
+
@v2 = Safrano::ServiceV2.new
|
195
179
|
copy_attribs_to @v2
|
196
180
|
end
|
197
181
|
|
@@ -220,17 +204,21 @@ module OData
|
|
220
204
|
(@v2.xserver_url = @xserver_url) if @v2
|
221
205
|
end
|
222
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
|
+
|
223
213
|
# end public API
|
224
214
|
|
225
215
|
def set_uribase
|
226
216
|
@uribase = if @xpath_prefix.empty?
|
227
217
|
@xserver_url
|
218
|
+
elsif @xpath_prefix[0] == '/'
|
219
|
+
"#{@xserver_url}#{@xpath_prefix}"
|
228
220
|
else
|
229
|
-
|
230
|
-
@xserver_url + @xpath_prefix
|
231
|
-
else
|
232
|
-
@xserver_url + '/' + @xpath_prefix
|
233
|
-
end
|
221
|
+
"#{@xserver_url}/#{@xpath_prefix}"
|
234
222
|
end
|
235
223
|
(@v1.uribase = @uribase) if @v1
|
236
224
|
(@v2.uribase = @uribase) if @v2
|
@@ -249,6 +237,8 @@ module OData
|
|
249
237
|
other.meta = ServiceMeta.new(other) # hum ... #todo: versions as well ?
|
250
238
|
other.relman = @relman
|
251
239
|
other.batch_handler = @batch_handler
|
240
|
+
other.complex_types = @complex_types
|
241
|
+
other.function_imports = @function_imports
|
252
242
|
other
|
253
243
|
end
|
254
244
|
|
@@ -261,38 +251,43 @@ module OData
|
|
261
251
|
def register_model(modelklass, entity_set_name = nil, is_media = false)
|
262
252
|
# check that the provided klass is a Sequel Model
|
263
253
|
|
264
|
-
raise(
|
254
|
+
raise(Safrano::API::ModelNameError, modelklass) unless modelklass.is_a? Sequel::Model::ClassMethods
|
265
255
|
|
266
|
-
if modelklass.ancestors.include?
|
256
|
+
if modelklass.ancestors.include? Safrano::Entity
|
267
257
|
# modules were already added previously;
|
268
258
|
# cleanup state to avoid having data from previous calls
|
269
259
|
# mostly usefull for testing (eg API)
|
270
260
|
modelklass.reset
|
271
261
|
elsif modelklass.primary_key.is_a?(Array) # first API call... (normal non-testing case)
|
272
|
-
modelklass.extend
|
273
|
-
modelklass.include
|
262
|
+
modelklass.extend Safrano::EntityClassMultiPK
|
263
|
+
modelklass.include Safrano::EntityMultiPK
|
274
264
|
else
|
275
|
-
modelklass.extend
|
276
|
-
modelklass.include
|
265
|
+
modelklass.extend Safrano::EntityClassSinglePK
|
266
|
+
modelklass.include Safrano::EntitySinglePK
|
277
267
|
end
|
268
|
+
|
278
269
|
# Media/Non-media
|
279
270
|
if is_media
|
280
|
-
modelklass.extend
|
271
|
+
modelklass.extend Safrano::EntityClassMedia
|
281
272
|
# set default media handler . Can be overridden later with the
|
282
273
|
# "use HandlerKlass, options" API
|
283
274
|
|
284
275
|
modelklass.set_default_media_handler
|
285
276
|
modelklass.api_check_media_fields
|
286
|
-
modelklass.include
|
277
|
+
modelklass.include Safrano::MediaEntity
|
287
278
|
else
|
288
|
-
modelklass.extend
|
289
|
-
modelklass.include
|
279
|
+
modelklass.extend Safrano::EntityClassNonMedia
|
280
|
+
modelklass.include Safrano::NonMediaEntity
|
290
281
|
end
|
291
282
|
|
292
283
|
modelklass.prepare_pk
|
293
284
|
modelklass.prepare_fields
|
294
285
|
esname = (entity_set_name || modelklass).to_s.freeze
|
295
|
-
|
286
|
+
serv_namespace = @xnamespace
|
287
|
+
modelklass.instance_eval do
|
288
|
+
@entity_set_name = esname
|
289
|
+
@namespace = serv_namespace
|
290
|
+
end
|
296
291
|
@cmap[esname] = modelklass
|
297
292
|
set_collections_sorted(@cmap.values)
|
298
293
|
end
|
@@ -313,6 +308,23 @@ module OData
|
|
313
308
|
modelklass.deferred_iblock = block if block_given?
|
314
309
|
end
|
315
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
|
+
|
316
328
|
def cmap=(imap)
|
317
329
|
@cmap = imap
|
318
330
|
set_collections_sorted(@cmap.values)
|
@@ -352,14 +364,13 @@ module OData
|
|
352
364
|
@collections.each(&:finalize_publishing)
|
353
365
|
|
354
366
|
# finalize the uri's and include NoMappingBeforeOutput or MappingBeforeOutput as needed
|
355
|
-
@collections.each
|
367
|
+
@collections.each do |klass|
|
356
368
|
klass.build_uri(@uribase)
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
}
|
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
|
363
374
|
|
364
375
|
# build allowed transitions (requires that @collections are filled and sorted for having a
|
365
376
|
# correct base_url_regexp)
|
@@ -368,11 +379,11 @@ module OData
|
|
368
379
|
# mixin adapter specific modules where needed
|
369
380
|
case Sequel::Model.db.adapter_scheme
|
370
381
|
when :postgres
|
371
|
-
|
382
|
+
Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreePostgres
|
372
383
|
when :sqlite
|
373
|
-
|
384
|
+
Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreeSqlite
|
374
385
|
else
|
375
|
-
|
386
|
+
Safrano::Filter::FuncTree.include Safrano::Filter::FuncTreeDefault
|
376
387
|
end
|
377
388
|
end
|
378
389
|
|
@@ -389,19 +400,24 @@ module OData
|
|
389
400
|
@collections.map(&:entity_set_name).join('|')
|
390
401
|
end
|
391
402
|
|
403
|
+
def base_url_func_regexp
|
404
|
+
@function_imports.keys.join('|')
|
405
|
+
end
|
406
|
+
|
392
407
|
def service
|
393
408
|
hres = {}
|
394
409
|
hres['d'] = { 'EntitySets' => @collections.map(&:type_name) }
|
395
410
|
hres
|
396
411
|
end
|
397
412
|
|
398
|
-
def service_xml(
|
413
|
+
def service_xml(_req)
|
399
414
|
doc = REXML::Document.new
|
400
415
|
# separator required ? ?
|
401
416
|
root = doc.add_element('service', 'xml:base' => @uribase)
|
402
417
|
|
403
418
|
root.add_namespace('xmlns:atom', XMLNS::W3_2005_ATOM)
|
404
419
|
root.add_namespace('xmlns:app', XMLNS::W3_2007_APP)
|
420
|
+
|
405
421
|
# this generates the main xmlns attribute
|
406
422
|
root.add_namespace(XMLNS::W3_2007_APP)
|
407
423
|
wp = root.add_element 'workspace'
|
@@ -412,7 +428,7 @@ module OData
|
|
412
428
|
@collections.each do |klass|
|
413
429
|
col = wp.add_element('collection', 'href' => klass.entity_set_name)
|
414
430
|
ct = col.add_element('atom:title')
|
415
|
-
ct.text = klass.
|
431
|
+
ct.text = klass.entity_set_name
|
416
432
|
end
|
417
433
|
|
418
434
|
XML_PREAMBLE + doc.to_pretty_xml
|
@@ -421,10 +437,18 @@ module OData
|
|
421
437
|
def add_metadata_xml_entity_type(schema)
|
422
438
|
@collections.each do |klass|
|
423
439
|
enty = klass.add_metadata_rexml(schema)
|
424
|
-
klass.add_metadata_navs_rexml(enty, @relman
|
440
|
+
klass.add_metadata_navs_rexml(enty, @relman)
|
425
441
|
end
|
426
442
|
end
|
427
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
|
+
|
428
452
|
def add_metadata_xml_associations(schema)
|
429
453
|
@relman.each_rel do |rel|
|
430
454
|
rel.with_metadata_info(@xnamespace) do |name, bdinfo|
|
@@ -447,7 +471,7 @@ module OData
|
|
447
471
|
# 3.a Entity set's
|
448
472
|
ec.add_element('EntitySet',
|
449
473
|
'Name' => klass.entity_set_name,
|
450
|
-
'EntityType' =>
|
474
|
+
'EntityType' => klass.type_name)
|
451
475
|
end
|
452
476
|
# 3.b Association set's
|
453
477
|
@relman.each_rel do |rel|
|
@@ -461,6 +485,9 @@ module OData
|
|
461
485
|
assoc.add_element('End', assoend)
|
462
486
|
end
|
463
487
|
end
|
488
|
+
|
489
|
+
# 4 function imports
|
490
|
+
add_metadata_xml_function_imports(ec)
|
464
491
|
end
|
465
492
|
|
466
493
|
def metadata_xml(_req)
|
@@ -494,9 +521,12 @@ module OData
|
|
494
521
|
schema = serv.add_element('Schema',
|
495
522
|
'Namespace' => @xnamespace,
|
496
523
|
'xmlns' => XMLNS::MSFT_ADO_2009_EDM)
|
497
|
-
# 1. all EntityType
|
524
|
+
# 1. a. all EntityType
|
498
525
|
add_metadata_xml_entity_type(schema)
|
499
526
|
|
527
|
+
# 1. b. all ComplexType
|
528
|
+
add_metadata_xml_complex_types(schema)
|
529
|
+
|
500
530
|
# 2. Associations
|
501
531
|
add_metadata_xml_associations(schema)
|
502
532
|
|
@@ -508,7 +538,7 @@ module OData
|
|
508
538
|
|
509
539
|
# methods related to transitions to next state (cf. walker)
|
510
540
|
module Transitions
|
511
|
-
DOLLAR_ID_REGEXP = Regexp.new('\A\/\$')
|
541
|
+
DOLLAR_ID_REGEXP = Regexp.new('\A\/\$')
|
512
542
|
ALLOWED_TRANSITIONS_FIXED = [
|
513
543
|
Safrano::TransitionEnd,
|
514
544
|
Safrano::TransitionMetadata,
|
@@ -517,18 +547,36 @@ module OData
|
|
517
547
|
].freeze
|
518
548
|
|
519
549
|
def build_allowed_transitions
|
520
|
-
@allowed_transitions =
|
521
|
-
|
522
|
-
|
523
|
-
|
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
|
524
563
|
end
|
525
564
|
|
526
565
|
def allowed_transitions
|
527
566
|
@allowed_transitions
|
528
567
|
end
|
529
568
|
|
569
|
+
def thread_safe_collection(collklass)
|
570
|
+
Safrano::OData::Collection.new(collklass)
|
571
|
+
end
|
572
|
+
|
530
573
|
def transition_collection(match_result)
|
531
|
-
[@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]
|
532
580
|
end
|
533
581
|
|
534
582
|
def transition_batch(_match_result)
|
@@ -544,7 +592,7 @@ module OData
|
|
544
592
|
end
|
545
593
|
|
546
594
|
def transition_end(_match_result)
|
547
|
-
|
595
|
+
Safrano::Transition::RESULT_END
|
548
596
|
end
|
549
597
|
end
|
550
598
|
|
@@ -552,14 +600,11 @@ module OData
|
|
552
600
|
|
553
601
|
def odata_get(req)
|
554
602
|
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)]]
|
603
|
+
# OData V2 reference service implementations are returning app-xml-u8
|
604
|
+
# so we do
|
605
|
+
[200, CT_APPXML, [service_xml(req)]]
|
561
606
|
else
|
562
|
-
# this is returned by http://services.odata.org/V2/OData/
|
607
|
+
# this is returned by http://services.odata.org/V2/OData/Safrano.svc
|
563
608
|
415
|
564
609
|
end
|
565
610
|
end
|
@@ -615,6 +660,7 @@ module OData
|
|
615
660
|
# a virtual entity for the service metadata
|
616
661
|
class ServiceMeta
|
617
662
|
attr_accessor :service
|
663
|
+
|
618
664
|
def initialize(service)
|
619
665
|
@service = service
|
620
666
|
end
|
@@ -625,7 +671,7 @@ module OData
|
|
625
671
|
end
|
626
672
|
|
627
673
|
def transition_end(_match_result)
|
628
|
-
|
674
|
+
Safrano::Transition::RESULT_END
|
629
675
|
end
|
630
676
|
|
631
677
|
def odata_get(req)
|