safrano 0.4.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/core_ext/Dir/iter.rb +18 -0
  3. data/lib/core_ext/Hash/transform.rb +21 -0
  4. data/lib/core_ext/Integer/edm.rb +13 -0
  5. data/lib/core_ext/REXML/Document/output.rb +16 -0
  6. data/lib/core_ext/String/convert.rb +25 -0
  7. data/lib/core_ext/String/edm.rb +13 -0
  8. data/lib/core_ext/dir.rb +3 -0
  9. data/lib/core_ext/hash.rb +3 -0
  10. data/lib/core_ext/integer.rb +3 -0
  11. data/lib/core_ext/rexml.rb +3 -0
  12. data/lib/core_ext/string.rb +5 -0
  13. data/lib/odata/attribute.rb +15 -10
  14. data/lib/odata/batch.rb +9 -7
  15. data/lib/odata/collection.rb +140 -591
  16. data/lib/odata/collection_filter.rb +18 -42
  17. data/lib/odata/collection_media.rb +111 -54
  18. data/lib/odata/collection_order.rb +5 -2
  19. data/lib/odata/common_logger.rb +2 -0
  20. data/lib/odata/complex_type.rb +152 -0
  21. data/lib/odata/edm/primitive_types.rb +184 -0
  22. data/lib/odata/entity.rb +123 -172
  23. data/lib/odata/error.rb +183 -32
  24. data/lib/odata/expand.rb +20 -17
  25. data/lib/odata/filter/base.rb +74 -0
  26. data/lib/odata/filter/error.rb +49 -6
  27. data/lib/odata/filter/parse.rb +41 -25
  28. data/lib/odata/filter/sequel.rb +133 -62
  29. data/lib/odata/filter/sequel_function_adapter.rb +148 -0
  30. data/lib/odata/filter/token.rb +26 -19
  31. data/lib/odata/filter/tree.rb +106 -52
  32. data/lib/odata/function_import.rb +168 -0
  33. data/lib/odata/model_ext.rb +639 -0
  34. data/lib/odata/navigation_attribute.rb +13 -26
  35. data/lib/odata/relations.rb +5 -5
  36. data/lib/odata/select.rb +17 -5
  37. data/lib/odata/transition.rb +71 -0
  38. data/lib/odata/url_parameters.rb +100 -24
  39. data/lib/odata/walker.rb +20 -10
  40. data/lib/safrano.rb +18 -38
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +23 -107
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +29 -33
  46. data/lib/safrano/rack_app.rb +66 -65
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
  48. data/lib/safrano/request.rb +96 -45
  49. data/lib/safrano/response.rb +4 -2
  50. data/lib/safrano/sequel_join_by_paths.rb +2 -2
  51. data/lib/safrano/service.rb +240 -130
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +6 -19
  54. metadata +32 -11
@@ -1,72 +1,47 @@
1
- #!/usr/bin/env ruby
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'rack'
4
- require_relative '../odata/walker.rb'
5
- require_relative 'request.rb'
6
- require_relative 'response.rb'
4
+ require_relative '../odata/walker'
5
+ require_relative 'request'
6
+ require_relative 'response'
7
7
 
8
- module OData
8
+ module Safrano
9
9
  # handle GET PUT etc
10
10
  module MethodHandlers
11
11
  def odata_options
12
- # cf. stackoverflow.com/questions/22924678/sinatra-delete-response-headers
13
-
14
- x = if @walker.status == :end
15
- headers.delete('Content-Type')
16
- @response.headers.delete('Content-Type')
17
- [200, EMPTY_HASH, '']
18
- else
19
- odata_error
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
- if @walker.status == :end
34
- @walker.end_context.odata_delete(@request)
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
- if @walker.status == :end
42
- @walker.end_context.odata_put(@request)
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
- if @walker.status == :end
50
- @walker.end_context.odata_patch(@request)
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
- if @walker.status == :end
58
- @walker.end_context.odata_get(@request)
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
- if @walker.status == :end
66
- @walker.end_context.odata_post(@request)
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
@@ -77,22 +52,21 @@ module OData
77
52
  # the main Rack server app. Source: the Rack docu/examples and partly
78
53
  # inspired from Sinatra
79
54
  class ServerApp
80
- METHODS_REGEXP = Regexp.new('HEAD|OPTIONS|GET|POST|PATCH|MERGE|PUT|DELETE')
55
+ METHODS_REGEXP = Regexp.new('HEAD|OPTIONS|GET|POST|PATCH|MERGE|PUT|DELETE').freeze
56
+ NOCACHE_HDRS = { 'Cache-Control' => 'no-cache',
57
+ 'Expires' => '-1',
58
+ 'Pragma' => 'no-cache' }.freeze
59
+ DATASERVICEVERSION = 'DataServiceVersion'
81
60
  include MethodHandlers
82
- def before
83
- headers 'Cache-Control' => 'no-cache'
84
- headers 'Expires' => '-1'
85
- headers 'Pragma' => 'no-cache'
86
61
 
62
+ def before
87
63
  @request.service_base = self.class.get_service_base
88
64
 
89
- neg_error = @request.negotiate_service_version
90
-
91
- raise RuntimeError if neg_error
92
-
93
- return false unless @request.service
94
-
95
- headers 'DataServiceVersion' => @request.service.data_service_version
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
96
70
  end
97
71
 
98
72
  # dispatch for all methods requiring parsing of the path
@@ -117,6 +91,11 @@ module OData
117
91
  end
118
92
  end
119
93
 
94
+ def dispatch_error(err)
95
+ @response.status, rsph, @response.body = err.odata_get(@request)
96
+ headers rsph
97
+ end
98
+
120
99
  def dispatch
121
100
  req_ret = if @request.request_method !~ METHODS_REGEXP
122
101
  [404, EMPTY_HASH, ['Did you get lost?']]
@@ -130,13 +109,23 @@ module OData
130
109
  end
131
110
 
132
111
  def call(env)
133
- @request = OData::Request.new(env)
134
- @response = OData::Response.new
112
+ # for thread safety
113
+ dup._call(env)
114
+ end
135
115
 
136
- before
116
+ def _call(env)
117
+ begin
118
+ @request = Safrano::Request.new(env)
119
+ @response = Safrano::Response.new
137
120
 
138
- dispatch
121
+ before.tap_error { |err| dispatch_error(err) }
122
+ .tap_valid { |res| dispatch }
139
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
140
129
  @response.finish
141
130
  end
142
131
 
@@ -165,10 +154,22 @@ module OData
165
154
  end
166
155
 
167
156
  def self.publish_service(&block)
168
- sbase = OData::ServiceBase.new
157
+ sbase = Safrano::ServiceBase.new
169
158
  sbase.instance_eval(&block) if block_given?
170
159
  sbase.finalize_publishing
171
160
  set_servicebase(sbase)
172
161
  end
173
162
  end
174
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 OData
7
+ module Safrano
6
8
  # just a Wrapper to ensure (force?) that mandatory middlewares are acutally
7
9
  # used
8
10
  class Builder < ::Rack::Builder
@@ -10,9 +12,23 @@ module Rack
10
12
  super(default_app) {}
11
13
  use ::Rack::Cors
12
14
  instance_eval(&block) if block_given?
13
- use ::Rack::Lint
14
15
  use ::Rack::ContentLength
15
16
  end
16
17
  end
17
18
  end
18
19
  end
20
+
21
+ # deprecated
22
+ # REMOVE 0.6
23
+ module Rack
24
+ module OData
25
+ class Builder < ::Rack::Safrano::Builder
26
+ def initialize(default_app = nil, &block)
27
+ ::Safrano::Deprecation.deprecate('Rack::OData::Builder',
28
+ 'Use Rack::Safrano::Builder instead')
29
+
30
+ super
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,13 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack'
2
4
  require 'rfc2047'
3
5
 
4
- module OData
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+(?:\.|\-|\+)?|\*)*'.freeze
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, path_info, @content_id_references)
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
@@ -164,44 +162,97 @@ module OData
164
162
  end
165
163
  end
166
164
 
167
- def negotiate_service_version
168
- maxv = if (rqv = env['HTTP_MAXDATASERVICEVERSION'])
169
- OData::ServiceBase.parse_data_service_version(rqv)
170
- else
171
- OData::MAX_DATASERVICE_VERSION
172
- end
173
- return OData::BadRequestError if maxv.nil?
174
- # client request an too old version --> 501
175
- return OData::NotImplementedError if maxv < OData::MIN_DATASERVICE_VERSION
176
-
177
- minv = if (rqv = env['HTTP_MINDATASERVICEVERSION'])
178
- OData::ServiceBase.parse_data_service_version(rqv)
179
- else
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
178
+ else
179
+ Contract.valid(maxv)
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
189
+
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
189
200
  else
190
- OData::MAX_DATASERVICE_VERSION
201
+ Contract.valid(v)
191
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
192
232
 
193
- return OData::BadRequestError if v.nil?
194
- return OData::NotImplementedError if v > OData::MAX_DATASERVICE_VERSION
195
- return OData::NotImplementedError if v < OData::MIN_DATASERVICE_VERSION
196
-
197
- @service = nil
198
- @service = case maxv
199
- when '1'
200
- @service_base.v1
201
- when '2', '3', '4'
202
- @service_base.v2
203
- end
204
- nil
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
@@ -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 OData
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['Content-Type'] ||= 'text/html'
17
+ headers[CONTENT_TYPE] ||= APPJSON_UTF8
16
18
  end
17
19
 
18
20
  def body=(value)
@@ -1,5 +1,5 @@
1
- #!/usr/bin/env ruby
1
+ # frozen_string_literal: true
2
2
 
3
- require_relative '../sequel/plugins/join_by_paths.rb'
3
+ require_relative '../sequel/plugins/join_by_paths'
4
4
 
5
5
  Sequel::Model.plugin Sequel::Plugins::JoinByPaths
@@ -1,97 +1,83 @@
1
- require 'rexml/document'
2
- require 'odata/relations.rb'
3
- require 'odata/batch.rb'
4
- require 'odata/error.rb'
1
+ # frozen_string_literal: true
5
2
 
6
- module OData
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+)\/?(.*)\z}.freeze
11
- def get_deferred_odata_h(entity:, attrib:, uribase:)
12
- { '__deferred' => { 'uri' => "#{entity.uri(uribase)}/#{attrib}" } }
13
- end
14
-
15
- # split expand argument into first and rest part
16
- def split_entity_expand_arg(exp_one)
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?
17
+ PATH_SPLITTER = %r{\A(\w+)/?(.*)\z}.freeze
18
+ DEFERRED = '__deferred'
19
+ URI = 'uri'
23
20
 
24
- k_s = cur_exp
25
-
26
- else
27
- k_s = exp_one.strip
28
- rest_exp = nil
29
- end
30
- k = k_s.to_sym
31
- yield k, k_s, 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
- def get_coll_odata_h(array:, template:, uribase:, icount: nil)
37
- res = array.map do |w|
38
- get_entity_odata_h(entity: w,
39
- template: template,
40
- uribase: uribase)
41
- end
42
- if icount
43
- { 'results' => res, '__count' => icount }
44
- else
45
- { 'results' => res }
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)
46
32
  end
33
+ icount ? { RESULTS_K => array, COUNT_K => icount } : { RESULTS_K => array }
47
34
  end
48
35
 
49
36
  # handle $links ... Note: $expand seems to be ignored when $links
50
37
  # are requested
51
- def get_entity_odata_link_h(entity:, uribase:)
52
- { uri: entity.uri(uribase) }
38
+ def get_entity_odata_link_h(entity:)
39
+ { uri: entity.uri }
53
40
  end
54
41
 
55
42
  EMPTYH = {}.freeze
56
- def get_entity_odata_h(entity:, template:, uribase:)
43
+ METADATA_K = '__metadata'
44
+ def get_entity_odata_h(entity:, template:)
57
45
  # start with metadata
58
- hres = { '__metadata' => entity.metadata_h(uribase: uribase) }
46
+ hres = { METADATA_K => entity.metadata_h }
59
47
 
60
48
  template.each do |elmt, arg|
61
49
  case elmt
62
50
  when :all_values
63
51
  hres.merge! entity.casted_values
52
+
64
53
  when :selected_vals
65
54
  hres.merge! entity.casted_values(arg)
55
+
66
56
  when :expand_e
67
57
 
68
58
  arg.each do |attr, templ|
69
59
  enval = entity.send(attr)
70
60
  hres[attr] = if enval
71
-
72
- get_entity_odata_h(entity: enval,
73
- template: templ,
74
- uribase: uribase)
61
+ get_entity_odata_h(entity: enval, template: templ)
75
62
  else
76
63
  # FK is NULL --> nav_value is nil --> return empty json
77
64
  EMPTYH
78
65
  end
79
66
  end
67
+
80
68
  when :expand_c
81
69
  arg.each do |attr, templ|
82
70
  next unless (encoll = entity.send(attr))
83
71
 
84
72
  # nav attributes that are a collection (x..n)
85
- hres[attr] = get_coll_odata_h(array: encoll,
86
- template: templ,
87
- uribase: uribase)
73
+ hres[attr] = get_coll_odata_h(array: encoll, template: templ)
88
74
  # else error ?
89
75
  end
76
+
90
77
  when :deferr
78
+ euri = entity.uri
91
79
  arg.each do |attr|
92
- hres[attr] = get_deferred_odata_h(entity: entity,
93
- attrib: attr,
94
- uribase: uribase)
80
+ hres[attr] = get_deferred_odata_h(entity_uri: euri, attrib: attr)
95
81
  end
96
82
  end
97
83
  end
@@ -100,33 +86,33 @@ module OData
100
86
  end
101
87
  end
102
88
 
103
- module OData
89
+ module Safrano
104
90
  # xml namespace constants needed for the output of XML service and
105
91
  # and metadata
106
92
  module XMLNS
107
- MSFT_ADO = 'http://schemas.microsoft.com/ado'.freeze
108
- MSFT_ADO_2009_EDM = "#{MSFT_ADO}/2009/11/edm".freeze
109
- MSFT_ADO_2007_EDMX = "#{MSFT_ADO}/2007/06/edmx".freeze
110
- MSFT_ADO_2007_META = MSFT_ADO + \
111
- '/2007/08/dataservices/metadata'.freeze
112
-
113
- W3_2005_ATOM = 'http://www.w3.org/2005/Atom'.freeze
114
- 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'
115
100
  end
116
101
  end
117
102
 
118
103
  # Link to Model
119
- module OData
120
- MAX_DATASERVICE_VERSION = '2'.freeze
121
- MIN_DATASERVICE_VERSION = '1'.freeze
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
122
109
  include XMLNS
123
110
  # Base class for service. Subclass will be for V1, V2 etc...
124
111
  class ServiceBase
125
112
  include Safrano
126
113
  include ExpandHandler
127
114
 
128
- XML_PREAMBLE = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>' + \
129
- "\r\n".freeze
115
+ XML_PREAMBLE = %Q(<?xml version="1.0" encoding="utf-8" standalone="yes"?>\r\n)
130
116
 
131
117
  # This is just a hash of entity Set Names to the corresponding Class
132
118
  # Example
@@ -144,10 +130,14 @@ module OData
144
130
  attr_accessor :xname
145
131
  attr_accessor :xnamespace
146
132
  attr_accessor :xpath_prefix
147
- # attr_accessor :xuribase
133
+ attr_accessor :xserver_url
134
+ attr_accessor :uribase
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,36 +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 = OData::Batch::DisabledHandler.new
165
- @relman = OData::RelationManager.new
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 = '/'.freeze
173
-
174
- # input is the DataServiceVersion request header string, eg.
175
- # '2.0;blabla' ---> Version -> 2
176
- def self.parse_data_service_version(inp)
177
- m = DATASERVICEVERSION_RGX.match(inp)
178
- m[1] if m
179
- end
163
+ DEFAULT_PATH_PREFIX = '/'
164
+ DEFAULT_SERVER_URL = 'http://localhost:9494'
180
165
 
181
166
  def enable_batch
182
- @batch_handler = OData::Batch::EnabledHandler.new
167
+ @batch_handler = Safrano::Batch::EnabledHandler.new
183
168
  (@v1.batch_handler = @batch_handler) if @v1
184
169
  (@v2.batch_handler = @batch_handler) if @v2
185
170
  end
186
171
 
187
172
  def enable_v1_service
188
- @v1 = OData::ServiceV1.new
173
+ @v1 = Safrano::ServiceV1.new
189
174
  copy_attribs_to @v1
190
175
  end
191
176
 
192
177
  def enable_v2_service
193
- @v2 = OData::ServiceV2.new
178
+ @v2 = Safrano::ServiceV2.new
194
179
  copy_attribs_to @v2
195
180
  end
196
181
 
@@ -212,56 +197,97 @@ module OData
212
197
  (@v1.xpath_prefix = @xpath_prefix) if @v1
213
198
  (@v2.xpath_prefix = @xpath_prefix) if @v2
214
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
+
215
213
  # end public API
216
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
+
217
227
  def copy_attribs_to(other)
218
228
  other.cmap = @cmap
219
229
  other.collections = @collections
230
+ other.allowed_transitions = @allowed_transitions
220
231
  other.xtitle = @xtitle
221
232
  other.xname = @xname
222
233
  other.xnamespace = @xnamespace
223
234
  other.xpath_prefix = @xpath_prefix
235
+ other.xserver_url = @xserver_url
236
+ other.uribase = @uribase
224
237
  other.meta = ServiceMeta.new(other) # hum ... #todo: versions as well ?
225
238
  other.relman = @relman
226
239
  other.batch_handler = @batch_handler
240
+ other.complex_types = @complex_types
241
+ other.function_imports = @function_imports
227
242
  other
228
243
  end
229
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
230
251
  def register_model(modelklass, entity_set_name = nil, is_media = false)
231
252
  # check that the provided klass is a Sequel Model
232
253
 
233
- raise(OData::API::ModelNameError, modelklass) unless modelklass.is_a? Sequel::Model::ClassMethods
254
+ raise(Safrano::API::ModelNameError, modelklass) unless modelklass.is_a? Sequel::Model::ClassMethods
234
255
 
235
- if modelklass.ancestors.include? OData::Entity
256
+ if modelklass.ancestors.include? Safrano::Entity
236
257
  # modules were already added previously;
237
258
  # cleanup state to avoid having data from previous calls
238
259
  # mostly usefull for testing (eg API)
239
260
  modelklass.reset
240
261
  elsif modelklass.primary_key.is_a?(Array) # first API call... (normal non-testing case)
241
- modelklass.extend OData::EntityClassMultiPK
242
- modelklass.include OData::EntityMultiPK
262
+ modelklass.extend Safrano::EntityClassMultiPK
263
+ modelklass.include Safrano::EntityMultiPK
243
264
  else
244
- modelklass.extend OData::EntityClassSinglePK
245
- modelklass.include OData::EntitySinglePK
265
+ modelklass.extend Safrano::EntityClassSinglePK
266
+ modelklass.include Safrano::EntitySinglePK
246
267
  end
268
+
247
269
  # Media/Non-media
248
270
  if is_media
249
- modelklass.extend OData::EntityClassMedia
271
+ modelklass.extend Safrano::EntityClassMedia
250
272
  # set default media handler . Can be overridden later with the
251
273
  # "use HandlerKlass, options" API
252
274
 
253
275
  modelklass.set_default_media_handler
254
276
  modelklass.api_check_media_fields
255
- modelklass.include OData::MediaEntity
277
+ modelklass.include Safrano::MediaEntity
256
278
  else
257
- modelklass.extend OData::EntityClassNonMedia
258
- modelklass.include OData::NonMediaEntity
279
+ modelklass.extend Safrano::EntityClassNonMedia
280
+ modelklass.include Safrano::NonMediaEntity
259
281
  end
260
282
 
261
283
  modelklass.prepare_pk
262
284
  modelklass.prepare_fields
263
285
  esname = (entity_set_name || modelklass).to_s.freeze
264
- modelklass.instance_eval { @entity_set_name = esname }
286
+ serv_namespace = @xnamespace
287
+ modelklass.instance_eval do
288
+ @entity_set_name = esname
289
+ @namespace = serv_namespace
290
+ end
265
291
  @cmap[esname] = modelklass
266
292
  set_collections_sorted(@cmap.values)
267
293
  end
@@ -282,6 +308,23 @@ module OData
282
308
  modelklass.deferred_iblock = block if block_given?
283
309
  end
284
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
+
285
328
  def cmap=(imap)
286
329
  @cmap = imap
287
330
  set_collections_sorted(@cmap.values)
@@ -313,10 +356,35 @@ module OData
313
356
  # set default path prefix if path_prefix was not called
314
357
  path_prefix(DEFAULT_PATH_PREFIX) unless @xpath_prefix
315
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
+
316
364
  @collections.each(&:finalize_publishing)
317
365
 
318
- # finalize the media handlers
319
- @collections.each { |klass| }
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
320
388
  end
321
389
 
322
390
  def execute_deferred_iblocks
@@ -332,19 +400,24 @@ module OData
332
400
  @collections.map(&:entity_set_name).join('|')
333
401
  end
334
402
 
403
+ def base_url_func_regexp
404
+ @function_imports.keys.join('|')
405
+ end
406
+
335
407
  def service
336
408
  hres = {}
337
409
  hres['d'] = { 'EntitySets' => @collections.map(&:type_name) }
338
410
  hres
339
411
  end
340
412
 
341
- def service_xml(req)
413
+ def service_xml(_req)
342
414
  doc = REXML::Document.new
343
415
  # separator required ? ?
344
- root = doc.add_element('service', 'xml:base' => req.uribase)
416
+ root = doc.add_element('service', 'xml:base' => @uribase)
345
417
 
346
418
  root.add_namespace('xmlns:atom', XMLNS::W3_2005_ATOM)
347
419
  root.add_namespace('xmlns:app', XMLNS::W3_2007_APP)
420
+
348
421
  # this generates the main xmlns attribute
349
422
  root.add_namespace(XMLNS::W3_2007_APP)
350
423
  wp = root.add_element 'workspace'
@@ -355,7 +428,7 @@ module OData
355
428
  @collections.each do |klass|
356
429
  col = wp.add_element('collection', 'href' => klass.entity_set_name)
357
430
  ct = col.add_element('atom:title')
358
- ct.text = klass.type_name
431
+ ct.text = klass.entity_set_name
359
432
  end
360
433
 
361
434
  XML_PREAMBLE + doc.to_pretty_xml
@@ -364,10 +437,18 @@ module OData
364
437
  def add_metadata_xml_entity_type(schema)
365
438
  @collections.each do |klass|
366
439
  enty = klass.add_metadata_rexml(schema)
367
- klass.add_metadata_navs_rexml(enty, @relman, @xnamespace)
440
+ klass.add_metadata_navs_rexml(enty, @relman)
368
441
  end
369
442
  end
370
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
+
371
452
  def add_metadata_xml_associations(schema)
372
453
  @relman.each_rel do |rel|
373
454
  rel.with_metadata_info(@xnamespace) do |name, bdinfo|
@@ -390,7 +471,7 @@ module OData
390
471
  # 3.a Entity set's
391
472
  ec.add_element('EntitySet',
392
473
  'Name' => klass.entity_set_name,
393
- 'EntityType' => "#{@xnamespace}.#{klass.type_name}")
474
+ 'EntityType' => klass.type_name)
394
475
  end
395
476
  # 3.b Association set's
396
477
  @relman.each_rel do |rel|
@@ -404,6 +485,9 @@ module OData
404
485
  assoc.add_element('End', assoend)
405
486
  end
406
487
  end
488
+
489
+ # 4 function imports
490
+ add_metadata_xml_function_imports(ec)
407
491
  end
408
492
 
409
493
  def metadata_xml(_req)
@@ -437,9 +521,12 @@ module OData
437
521
  schema = serv.add_element('Schema',
438
522
  'Namespace' => @xnamespace,
439
523
  'xmlns' => XMLNS::MSFT_ADO_2009_EDM)
440
- # 1. all EntityType
524
+ # 1. a. all EntityType
441
525
  add_metadata_xml_entity_type(schema)
442
526
 
527
+ # 1. b. all ComplexType
528
+ add_metadata_xml_complex_types(schema)
529
+
443
530
  # 2. Associations
444
531
  add_metadata_xml_associations(schema)
445
532
 
@@ -451,20 +538,45 @@ module OData
451
538
 
452
539
  # methods related to transitions to next state (cf. walker)
453
540
  module Transitions
454
- DOLLAR_ID_REGEXP = Regexp.new('\A\/\$').freeze
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
+
455
565
  def allowed_transitions
456
- @allowed_transitions = [
457
- Safrano::TransitionEnd,
458
- Safrano::TransitionMetadata,
459
- Safrano::TransitionBatch,
460
- Safrano::TransitionContentId,
461
- Safrano::Transition.new(%r{\A/(#{base_url_regexp})(.*)},
462
- trans: 'transition_collection')
463
- ]
566
+ @allowed_transitions
567
+ end
568
+
569
+ def thread_safe_collection(collklass)
570
+ Safrano::OData::Collection.new(collklass)
464
571
  end
465
572
 
466
573
  def transition_collection(match_result)
467
- [@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]
468
580
  end
469
581
 
470
582
  def transition_batch(_match_result)
@@ -480,7 +592,7 @@ module OData
480
592
  end
481
593
 
482
594
  def transition_end(_match_result)
483
- [nil, :end]
595
+ Safrano::Transition::RESULT_END
484
596
  end
485
597
  end
486
598
 
@@ -488,14 +600,11 @@ module OData
488
600
 
489
601
  def odata_get(req)
490
602
  if req.accept?(APPXML)
491
- # app.headers 'Content-Type' => 'application/xml;charset=utf-8'
492
- # Doc: 2.2.3.7.1 Service Document As per [RFC5023], AtomPub Service
493
- # Documents MUST be
494
- # identified with the "application/atomsvc+xml" media type (see
495
- # [RFC5023] section 8).
496
- [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)]]
497
606
  else
498
- # this is returned by http://services.odata.org/V2/OData/OData.svc
607
+ # this is returned by http://services.odata.org/V2/OData/Safrano.svc
499
608
  415
500
609
  end
501
610
  end
@@ -507,18 +616,18 @@ module OData
507
616
  @data_service_version = '1.0'
508
617
  end
509
618
 
510
- def get_coll_odata_links_h(array:, uribase:, icount: nil)
619
+ def get_coll_odata_links_h(array:, icount: nil)
511
620
  array.map do |w|
512
- get_entity_odata_link_h(entity: w, uribase: uribase)
621
+ get_entity_odata_link_h(entity: w)
513
622
  end
514
623
  end
515
624
 
516
- def get_coll_odata_h(array:, template:, uribase:, icount: nil)
517
- array.map do |w|
625
+ def get_coll_odata_h(array:, template:, icount: nil)
626
+ array.map! do |w|
518
627
  get_entity_odata_h(entity: w,
519
- template: template,
520
- uribase: uribase)
628
+ template: template)
521
629
  end
630
+ array
522
631
  end
523
632
 
524
633
  def get_emptycoll_odata_h
@@ -532,36 +641,37 @@ module OData
532
641
  @data_service_version = '2.0'
533
642
  end
534
643
 
535
- def get_coll_odata_links_h(array:, uribase:, icount: nil)
644
+ def get_coll_odata_links_h(array:, icount: nil)
536
645
  res = array.map do |w|
537
- get_entity_odata_link_h(entity: w, uribase: uribase)
646
+ get_entity_odata_link_h(entity: w)
538
647
  end
539
648
  if icount
540
- { 'results' => res, '__count' => icount }
649
+ { RESULTS_K => res, COUNT_K => icount }
541
650
  else
542
- { 'results' => res }
651
+ { RESULTS_K => res }
543
652
  end
544
653
  end
545
654
 
546
655
  def get_emptycoll_odata_h
547
- { 'results' => EMPTY_HASH_IN_ARY }
656
+ { RESULTS_K => EMPTY_HASH_IN_ARY }
548
657
  end
549
658
  end
550
659
 
551
660
  # a virtual entity for the service metadata
552
661
  class ServiceMeta
553
662
  attr_accessor :service
663
+
554
664
  def initialize(service)
555
665
  @service = service
556
666
  end
557
667
 
668
+ ALLOWED_TRANSITIONS_FIXED = [Safrano::TransitionEnd].freeze
558
669
  def allowed_transitions
559
- @allowed_transitions = [Safrano::Transition.new('',
560
- trans: 'transition_end')]
670
+ ALLOWED_TRANSITIONS_FIXED
561
671
  end
562
672
 
563
673
  def transition_end(_match_result)
564
- [nil, :end]
674
+ Safrano::Transition::RESULT_END
565
675
  end
566
676
 
567
677
  def odata_get(req)